dd-trace 5.87.0 → 5.88.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 +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +225 -4
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
- package/packages/datadog-instrumentations/src/jest.js +76 -12
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
- package/packages/datadog-plugin-cypress/src/support.js +48 -8
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- package/packages/dd-trace/src/azure_metadata.js +0 -2
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -197
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +36 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +14 -1
- package/packages/dd-trace/src/encode/agentless-json.js +141 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +15 -2
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- package/packages/dd-trace/src/scope/noop/scope.js +0 -21
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const transforms = require('./transforms')
|
|
4
|
+
|
|
5
|
+
function transform (state, ...args) {
|
|
6
|
+
const operator = state.operator = getOperator(state)
|
|
7
|
+
|
|
8
|
+
transforms[operator](state, ...args)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getOperator ({ functionQuery: { kind } }) {
|
|
12
|
+
switch (kind) {
|
|
13
|
+
case 'Async': return 'tracePromise'
|
|
14
|
+
case 'AsyncIterator': return 'traceAsyncIterator'
|
|
15
|
+
case 'Callback': return 'traceCallback'
|
|
16
|
+
case 'Iterator': return 'traceIterator'
|
|
17
|
+
case 'Sync': return 'traceSync'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { transform }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { parse, query } = require('./compiler')
|
|
3
|
+
const { parse, query, traverse } = require('./compiler')
|
|
4
4
|
|
|
5
5
|
const tracingChannelPredicate = (node) => (
|
|
6
6
|
node.specifiers?.[0]?.local?.name === 'tr_ch_apm_tracingChannel' ||
|
|
@@ -8,15 +8,15 @@ const tracingChannelPredicate = (node) => (
|
|
|
8
8
|
)
|
|
9
9
|
|
|
10
10
|
const transforms = module.exports = {
|
|
11
|
-
tracingChannelImport ({
|
|
11
|
+
tracingChannelImport ({ sourceType }, node) {
|
|
12
12
|
if (node.body.some(tracingChannelPredicate)) return
|
|
13
13
|
|
|
14
14
|
const index = node.body.findIndex(child => child.directive === 'use strict')
|
|
15
|
-
const code =
|
|
15
|
+
const code = sourceType === 'module'
|
|
16
16
|
? 'import { tracingChannel as tr_ch_apm_tracingChannel } from "diagnostics_channel"'
|
|
17
17
|
: 'const {tracingChannel: tr_ch_apm_tracingChannel} = require("diagnostics_channel")'
|
|
18
18
|
|
|
19
|
-
node.body.splice(index + 1, 0, parse(code, {
|
|
19
|
+
node.body.splice(index + 1, 0, parse(code, { sourceType }).body[0])
|
|
20
20
|
},
|
|
21
21
|
|
|
22
22
|
tracingChannelDeclaration (state, node) {
|
|
@@ -35,7 +35,9 @@ const transforms = module.exports = {
|
|
|
35
35
|
node.body.splice(index + 1, 0, parse(code).body[0])
|
|
36
36
|
},
|
|
37
37
|
|
|
38
|
+
traceAsyncIterator: traceAny,
|
|
38
39
|
traceCallback: traceAny,
|
|
40
|
+
traceIterator: traceAny,
|
|
39
41
|
tracePromise: traceAny,
|
|
40
42
|
traceSync: traceAny,
|
|
41
43
|
}
|
|
@@ -51,18 +53,25 @@ function traceAny (state, node, _parent, ancestry) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function traceFunction (state, node, program) {
|
|
54
|
-
const { operator } = state
|
|
55
|
-
|
|
56
56
|
transforms.tracingChannelDeclaration(state, program)
|
|
57
57
|
|
|
58
58
|
node.body = wrap(state, {
|
|
59
|
-
type: '
|
|
59
|
+
type: 'FunctionExpression',
|
|
60
60
|
params: node.params,
|
|
61
61
|
body: node.body,
|
|
62
|
-
async:
|
|
62
|
+
async: node.async,
|
|
63
63
|
expression: false,
|
|
64
|
-
generator:
|
|
65
|
-
})
|
|
64
|
+
generator: node.generator,
|
|
65
|
+
}, program)
|
|
66
|
+
|
|
67
|
+
// The original function no longer contains any calls to `await` or `yield` as
|
|
68
|
+
// the function body is copied to the internal wrapped function, so we set
|
|
69
|
+
// these to false to avoid altering the return value of the wrapper. The old
|
|
70
|
+
// values are instead copied to the new AST node above.
|
|
71
|
+
node.generator = false
|
|
72
|
+
node.async = false
|
|
73
|
+
|
|
74
|
+
wrapSuper(state, node)
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
function traceInstanceMethod (state, node, program) {
|
|
@@ -100,15 +109,19 @@ function traceInstanceMethod (state, node, program) {
|
|
|
100
109
|
const fn = ctorBody[1].expression.right
|
|
101
110
|
|
|
102
111
|
fn.async = operator === 'tracePromise'
|
|
103
|
-
fn.body = wrap(state, { type: 'Identifier', name: `__apm$${methodName}` })
|
|
112
|
+
fn.body = wrap(state, { type: 'Identifier', name: `__apm$${methodName}` }, program)
|
|
113
|
+
|
|
114
|
+
wrapSuper(state, fn)
|
|
104
115
|
|
|
105
116
|
ctor.value.body.body.push(...ctorBody)
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
function wrap (state, node) {
|
|
119
|
+
function wrap (state, node, program) {
|
|
109
120
|
const { channelName, operator } = state
|
|
110
121
|
|
|
122
|
+
if (operator === 'traceAsyncIterator') return wrapIterator(state, node, program)
|
|
111
123
|
if (operator === 'traceCallback') return wrapCallback(state, node)
|
|
124
|
+
if (operator === 'traceIterator') return wrapIterator(state, node, program)
|
|
112
125
|
|
|
113
126
|
const async = operator === 'tracePromise' ? 'async' : ''
|
|
114
127
|
const channelVariable = 'tr_ch_apm$' + channelName.replaceAll(':', '_')
|
|
@@ -133,6 +146,55 @@ function wrap (state, node) {
|
|
|
133
146
|
return wrapper
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
function wrapSuper (_state, node) {
|
|
150
|
+
const members = new Set()
|
|
151
|
+
|
|
152
|
+
traverse(
|
|
153
|
+
node.body,
|
|
154
|
+
'[object.type=Super]',
|
|
155
|
+
(node, parent) => {
|
|
156
|
+
const { name } = node.property
|
|
157
|
+
|
|
158
|
+
let child
|
|
159
|
+
|
|
160
|
+
if (parent.callee) {
|
|
161
|
+
// This is needed because for generator functions we have to move the
|
|
162
|
+
// original function to a nested wrapped function, but we can't use an
|
|
163
|
+
// arrow function because arrow function cannot be generator functions,
|
|
164
|
+
// and `super` cannot be called from a nested function, so we have to
|
|
165
|
+
// rewrite any `super` call to not use the keyword.
|
|
166
|
+
const { expression } = parse(`__apm$super['${name}'].call(this)`).body[0]
|
|
167
|
+
|
|
168
|
+
parent.callee = child = expression.callee
|
|
169
|
+
parent.arguments.unshift(...expression.arguments)
|
|
170
|
+
} else {
|
|
171
|
+
parent.expression = child = parse(`__apm$super['${name}']`).body[0]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
child.computed = parent.callee.computed
|
|
175
|
+
child.optional = parent.callee.optional
|
|
176
|
+
|
|
177
|
+
members.add(name)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
for (const name of members) {
|
|
182
|
+
const member = parse(`
|
|
183
|
+
class Wrapper {
|
|
184
|
+
wrapper () {
|
|
185
|
+
__apm$super['${name}'] = super['${name}']
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
`).body[0].body.body[0].value.body.body[0]
|
|
189
|
+
|
|
190
|
+
node.body.body.unshift(member)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (members.size > 0) {
|
|
194
|
+
node.body.body.unshift(parse('const __apm$super = {}').body[0])
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
136
198
|
function wrapCallback (state, node) {
|
|
137
199
|
const { channelName, functionQuery: { index = -1 } } = state
|
|
138
200
|
const channelVariable = 'tr_ch_apm$' + channelName.replaceAll(':', '_')
|
|
@@ -194,3 +256,67 @@ function wrapCallback (state, node) {
|
|
|
194
256
|
|
|
195
257
|
return wrapper
|
|
196
258
|
}
|
|
259
|
+
|
|
260
|
+
function wrapIterator (state, node, program) {
|
|
261
|
+
const { channelName, operator } = state
|
|
262
|
+
const baseChannel = channelName.replaceAll(':', '_')
|
|
263
|
+
const channelVariable = 'tr_ch_apm$' + baseChannel
|
|
264
|
+
const nextChannel = baseChannel + '_next'
|
|
265
|
+
const traceMethod = operator === 'traceAsyncIterator' ? 'tracePromise' : 'traceSync'
|
|
266
|
+
const traceNext = `tr_ch_apm$${nextChannel}.${traceMethod}`
|
|
267
|
+
|
|
268
|
+
transforms.tracingChannelDeclaration({ ...state, channelName: nextChannel }, program)
|
|
269
|
+
|
|
270
|
+
const wrapper = parse(`
|
|
271
|
+
function wrapper () {
|
|
272
|
+
const __apm$traced = () => {
|
|
273
|
+
const __apm$wrapped = () => {};
|
|
274
|
+
return __apm$wrapped.apply(this, arguments);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (!${channelVariable}.start.hasSubscribers) return __apm$traced();
|
|
278
|
+
|
|
279
|
+
{
|
|
280
|
+
const wrap = iter => {
|
|
281
|
+
const { next: iterNext, return: iterReturn, throw: iterThrow } = iter;
|
|
282
|
+
|
|
283
|
+
iter.next = (...args) => ${traceNext}(iterNext, ctx, iter, ...args);
|
|
284
|
+
iter.return = (...args) => ${traceNext}(iterReturn, ctx, iter, ...args);
|
|
285
|
+
iter.throw = (...args) => ${traceNext}(iterThrow, ctx, iter, ...args);
|
|
286
|
+
|
|
287
|
+
return iter;
|
|
288
|
+
};
|
|
289
|
+
const ctx = {
|
|
290
|
+
arguments,
|
|
291
|
+
self: this,
|
|
292
|
+
moduleVersion: "1.0.0"
|
|
293
|
+
};
|
|
294
|
+
const iter = ${channelVariable}.traceSync(__apm$traced, ctx);
|
|
295
|
+
|
|
296
|
+
if (typeof iter.then !== 'function') return wrap(iter);
|
|
297
|
+
|
|
298
|
+
return iter.then(result => {
|
|
299
|
+
ctx.result = result;
|
|
300
|
+
|
|
301
|
+
${channelVariable}.asyncStart.publish(ctx);
|
|
302
|
+
${channelVariable}.asyncEnd.publish(ctx);
|
|
303
|
+
|
|
304
|
+
return wrap(result);
|
|
305
|
+
}, err => {
|
|
306
|
+
ctx.error = err;
|
|
307
|
+
|
|
308
|
+
${channelVariable}.error.publish(ctx);
|
|
309
|
+
${channelVariable}.asyncStart.publish(ctx);
|
|
310
|
+
${channelVariable}.asyncEnd.publish(ctx);
|
|
311
|
+
|
|
312
|
+
return Promise.reject(err);
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
`).body[0].body // Extract only block statement of function body.
|
|
317
|
+
|
|
318
|
+
// Replace the right-hand side assignment of `const __apm$wrapped = () => {}`.
|
|
319
|
+
query(wrapper, '[id.name=__apm$wrapped]')[0].init = node
|
|
320
|
+
|
|
321
|
+
return wrapper
|
|
322
|
+
}
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
getFormattedJestTestParameters,
|
|
22
22
|
getJestTestName,
|
|
23
23
|
getJestSuitesToRun,
|
|
24
|
+
getEfdRetryCount,
|
|
24
25
|
} = require('../../datadog-plugin-jest/src/util')
|
|
25
26
|
const { addHook, channel } = require('./helpers/instrument')
|
|
26
27
|
|
|
@@ -76,6 +77,7 @@ let hasUnskippableSuites = false
|
|
|
76
77
|
let hasForcedToRunSuites = false
|
|
77
78
|
let isEarlyFlakeDetectionEnabled = false
|
|
78
79
|
let earlyFlakeDetectionNumRetries = 0
|
|
80
|
+
let earlyFlakeDetectionSlowTestRetries = {}
|
|
79
81
|
let earlyFlakeDetectionFaultyThreshold = 30
|
|
80
82
|
let isEarlyFlakeDetectionFaulty = false
|
|
81
83
|
let hasFilteredSkippableSuites = false
|
|
@@ -95,6 +97,12 @@ const attemptToFixRetriedTestsStatuses = new Map()
|
|
|
95
97
|
const wrappedWorkers = new WeakSet()
|
|
96
98
|
const testSuiteMockedFiles = new Map()
|
|
97
99
|
const testsToBeRetried = new Set()
|
|
100
|
+
// Per-test: how many EFD retries were determined after the first execution.
|
|
101
|
+
const efdDeterminedRetries = new Map()
|
|
102
|
+
// Tests whose first run exceeded the 5-min threshold — tagged "slow".
|
|
103
|
+
const efdSlowAbortedTests = new Set()
|
|
104
|
+
// Tests added as EFD new-test candidates (not ATF, not impacted).
|
|
105
|
+
const efdNewTestCandidates = new Set()
|
|
98
106
|
const testSuiteAbsolutePathsWithFastCheck = new Set()
|
|
99
107
|
const testSuiteJestObjects = new Map()
|
|
100
108
|
|
|
@@ -197,7 +205,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
197
205
|
this.isImpactedTestsEnabled = this.testEnvironmentOptions._ddIsImpactedTestsEnabled
|
|
198
206
|
|
|
199
207
|
if (this.isKnownTestsEnabled) {
|
|
200
|
-
|
|
208
|
+
earlyFlakeDetectionSlowTestRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionSlowTestRetries ?? {}
|
|
201
209
|
try {
|
|
202
210
|
this.knownTestsForThisSuite = this.getKnownTestsForSuite(this.testEnvironmentOptions._ddKnownTests)
|
|
203
211
|
|
|
@@ -466,7 +474,13 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
466
474
|
testContexts.set(event.test, ctx)
|
|
467
475
|
|
|
468
476
|
testStartCh.runStores(ctx, () => {
|
|
469
|
-
|
|
477
|
+
let p = event.test.parent
|
|
478
|
+
const hooks = []
|
|
479
|
+
while (p != null) {
|
|
480
|
+
hooks.push(...p.hooks)
|
|
481
|
+
p = p.parent
|
|
482
|
+
}
|
|
483
|
+
for (const hook of hooks) {
|
|
470
484
|
let hookFn = hook.fn
|
|
471
485
|
if (originalHookFns.has(hook)) {
|
|
472
486
|
hookFn = originalHookFns.get(hook)
|
|
@@ -537,11 +551,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
537
551
|
retriedTestsToNumAttempts.set(testFullName, 0)
|
|
538
552
|
if (this.isEarlyFlakeDetectionEnabled) {
|
|
539
553
|
testsToBeRetried.add(testFullName)
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
retryType: 'Early flake detection',
|
|
544
|
-
})
|
|
554
|
+
efdNewTestCandidates.add(testFullName)
|
|
555
|
+
// Cloning is deferred to test_done after the first execution,
|
|
556
|
+
// when we know the duration and can choose the right retry count.
|
|
545
557
|
}
|
|
546
558
|
}
|
|
547
559
|
}
|
|
@@ -566,8 +578,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
566
578
|
let attemptToFixFailed = false
|
|
567
579
|
let failedAllTests = false
|
|
568
580
|
let isAttemptToFix = false
|
|
581
|
+
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
|
|
569
582
|
if (this.isTestManagementTestsEnabled) {
|
|
570
|
-
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
|
|
571
583
|
isAttemptToFix = this.testManagementTestsForThisSuite?.attemptToFix?.includes(testName)
|
|
572
584
|
if (isAttemptToFix) {
|
|
573
585
|
if (attemptToFixRetriedTestsStatuses.has(testName)) {
|
|
@@ -592,9 +604,53 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
592
604
|
}
|
|
593
605
|
}
|
|
594
606
|
|
|
607
|
+
// EFD dynamic cloning: on first execution of a new EFD candidate,
|
|
608
|
+
// determine the retry count from the test's duration.
|
|
609
|
+
if (
|
|
610
|
+
this.isEarlyFlakeDetectionEnabled &&
|
|
611
|
+
this.isKnownTestsEnabled &&
|
|
612
|
+
efdNewTestCandidates.has(testName) &&
|
|
613
|
+
event.test.invocations === 1 &&
|
|
614
|
+
!efdDeterminedRetries.has(testName)
|
|
615
|
+
) {
|
|
616
|
+
const durationMs = event.test.duration ?? 0
|
|
617
|
+
const retryCount = getEfdRetryCount(durationMs, earlyFlakeDetectionSlowTestRetries)
|
|
618
|
+
efdDeterminedRetries.set(testName, retryCount)
|
|
619
|
+
if (retryCount > 0) {
|
|
620
|
+
// Temporarily adjust jest-circus state so that retry tests are registered
|
|
621
|
+
// into the correct describe block and bypass the "tests have started" guard.
|
|
622
|
+
//
|
|
623
|
+
// Problem 1 (jest-circus ≤24): currentDescribeBlock points to ROOT during
|
|
624
|
+
// execution, and ROOT's tests loop already finished before children ran.
|
|
625
|
+
//
|
|
626
|
+
// Problem 2 (jest-circus ≥27): `hasStarted = true` causes `test()` to throw
|
|
627
|
+
// "Cannot add a test after tests have started running".
|
|
628
|
+
//
|
|
629
|
+
// Fix: temporarily point currentDescribeBlock to the test's parent (so retries
|
|
630
|
+
// land in the still-iterating children array) and set hasStarted = false (so the
|
|
631
|
+
// guard is bypassed). Both are restored immediately after scheduling the retries.
|
|
632
|
+
const originalDescribeBlock = state.currentDescribeBlock
|
|
633
|
+
const originalHasStarted = state.hasStarted
|
|
634
|
+
state.currentDescribeBlock = event.test.parent ?? originalDescribeBlock
|
|
635
|
+
state.hasStarted = false
|
|
636
|
+
this.retryTest({
|
|
637
|
+
jestEvent: {
|
|
638
|
+
testName: event.test.name,
|
|
639
|
+
fn: event.test.fn,
|
|
640
|
+
timeout: event.test.timeout,
|
|
641
|
+
},
|
|
642
|
+
retryCount,
|
|
643
|
+
retryType: 'Early flake detection',
|
|
644
|
+
})
|
|
645
|
+
state.currentDescribeBlock = originalDescribeBlock
|
|
646
|
+
state.hasStarted = originalHasStarted
|
|
647
|
+
} else {
|
|
648
|
+
efdSlowAbortedTests.add(testName)
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
595
652
|
let isEfdRetry = false
|
|
596
653
|
// We'll store the test statuses of the retries
|
|
597
|
-
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
|
|
598
654
|
if (this.isKnownTestsEnabled) {
|
|
599
655
|
const isNewTest = retriedTestsToNumAttempts.has(testName)
|
|
600
656
|
if (isNewTest) {
|
|
@@ -607,7 +663,8 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
607
663
|
const testStatuses = newTestsTestStatuses.get(testName)
|
|
608
664
|
// Check if this is the last EFD retry.
|
|
609
665
|
// If it is, we'll set the failedAllTests flag to true if all the tests failed
|
|
610
|
-
|
|
666
|
+
const efdRetryCount = efdDeterminedRetries.get(testName) ?? 0
|
|
667
|
+
if (efdRetryCount > 0 && testStatuses.length === efdRetryCount + 1 &&
|
|
611
668
|
testStatuses.every(status => status === 'fail')) {
|
|
612
669
|
failedAllTests = true
|
|
613
670
|
}
|
|
@@ -665,6 +722,7 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
665
722
|
attemptToFixFailed,
|
|
666
723
|
isAtrRetry,
|
|
667
724
|
finalStatus,
|
|
725
|
+
earlyFlakeAbortReason: efdSlowAbortedTests.has(testName) ? 'slow' : undefined,
|
|
668
726
|
})
|
|
669
727
|
|
|
670
728
|
if (promises.isProbeReady) {
|
|
@@ -676,6 +734,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
676
734
|
test.errors = errors
|
|
677
735
|
}
|
|
678
736
|
atrSuppressedErrors.clear()
|
|
737
|
+
efdDeterminedRetries.clear()
|
|
738
|
+
efdSlowAbortedTests.clear()
|
|
739
|
+
efdNewTestCandidates.clear()
|
|
679
740
|
}
|
|
680
741
|
if (event.name === 'test_skip' || event.name === 'test_todo') {
|
|
681
742
|
const testName = getJestTestName(event.test, this.getShouldStripSeedFromTestName())
|
|
@@ -696,7 +757,9 @@ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
|
|
|
696
757
|
getEfdResult ({ testName, isNewTest, isModifiedTest, isEfdRetry, numberOfExecutedRetries }) {
|
|
697
758
|
const isEfdEnabled = this.isEarlyFlakeDetectionEnabled
|
|
698
759
|
const isEfdActive = isEfdEnabled && (isNewTest || isModifiedTest)
|
|
699
|
-
const
|
|
760
|
+
const retryCount = efdDeterminedRetries.get(testName) ?? 0
|
|
761
|
+
const isSlowAbort = efdSlowAbortedTests.has(testName)
|
|
762
|
+
const isLastEfdRetry = (isEfdRetry && numberOfExecutedRetries >= (retryCount + 1)) || isSlowAbort
|
|
700
763
|
const isFinalEfdTestExecution = isEfdActive && isLastEfdRetry
|
|
701
764
|
|
|
702
765
|
let finalStatus
|
|
@@ -933,6 +996,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
933
996
|
isSuitesSkippingEnabled = libraryConfig.isSuitesSkippingEnabled
|
|
934
997
|
isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
|
|
935
998
|
earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
|
|
999
|
+
earlyFlakeDetectionSlowTestRetries = libraryConfig.earlyFlakeDetectionSlowTestRetries ?? {}
|
|
936
1000
|
earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
|
|
937
1001
|
isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
|
|
938
1002
|
isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
|
|
@@ -1508,7 +1572,7 @@ addHook({
|
|
|
1508
1572
|
_ddItrCorrelationId,
|
|
1509
1573
|
_ddKnownTests,
|
|
1510
1574
|
_ddIsEarlyFlakeDetectionEnabled,
|
|
1511
|
-
|
|
1575
|
+
_ddEarlyFlakeDetectionSlowTestRetries,
|
|
1512
1576
|
_ddRepositoryRoot,
|
|
1513
1577
|
_ddIsFlakyTestRetriesEnabled,
|
|
1514
1578
|
_ddFlakyTestRetriesCount,
|
|
@@ -24,22 +24,6 @@ const batchConsumerErrorCh = channel('apm:kafkajs:consume-batch:error')
|
|
|
24
24
|
|
|
25
25
|
const disabledHeaderWeakSet = new WeakSet()
|
|
26
26
|
|
|
27
|
-
function commitsFromEvent (event) {
|
|
28
|
-
const { payload: { groupId, topics } } = event
|
|
29
|
-
const commitList = []
|
|
30
|
-
for (const { topic, partitions } of topics) {
|
|
31
|
-
for (const { partition, offset } of partitions) {
|
|
32
|
-
commitList.push({
|
|
33
|
-
groupId,
|
|
34
|
-
partition,
|
|
35
|
-
offset,
|
|
36
|
-
topic,
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
consumerCommitCh.publish(commitList)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
27
|
addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => {
|
|
44
28
|
class Kafka extends BaseKafka {
|
|
45
29
|
constructor (options) {
|
|
@@ -132,6 +116,7 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
|
|
|
132
116
|
}
|
|
133
117
|
|
|
134
118
|
const kafkaClusterIdPromise = getKafkaClusterId(this)
|
|
119
|
+
let resolvedClusterId = null
|
|
135
120
|
|
|
136
121
|
const eachMessageExtractor = (args, clusterId) => {
|
|
137
122
|
const { topic, partition, message } = args[0]
|
|
@@ -146,13 +131,31 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
|
|
|
146
131
|
|
|
147
132
|
const consumer = createConsumer.apply(this, arguments)
|
|
148
133
|
|
|
149
|
-
consumer.on(consumer.events.COMMIT_OFFSETS,
|
|
134
|
+
consumer.on(consumer.events.COMMIT_OFFSETS, (event) => {
|
|
135
|
+
const { payload: { groupId: commitGroupId, topics } } = event
|
|
136
|
+
const commitList = []
|
|
137
|
+
for (const { topic, partitions } of topics) {
|
|
138
|
+
for (const { partition, offset } of partitions) {
|
|
139
|
+
commitList.push({
|
|
140
|
+
groupId: commitGroupId,
|
|
141
|
+
partition,
|
|
142
|
+
offset,
|
|
143
|
+
topic,
|
|
144
|
+
clusterId: resolvedClusterId,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
consumerCommitCh.publish(commitList)
|
|
149
|
+
})
|
|
150
150
|
|
|
151
151
|
const run = consumer.run
|
|
152
152
|
const groupId = arguments[0].groupId
|
|
153
153
|
|
|
154
154
|
consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) {
|
|
155
155
|
const wrapConsume = (clusterId) => {
|
|
156
|
+
// In kafkajs COMMIT_OFFSETS always happens in the context of one synchronous run
|
|
157
|
+
// So this will always reference a correct cluster id
|
|
158
|
+
resolvedClusterId = clusterId
|
|
156
159
|
return run({
|
|
157
160
|
eachMessage: wrappedCallback(
|
|
158
161
|
eachMessage,
|
|
@@ -41,7 +41,7 @@ const testSuiteToTestStatuses = new Map()
|
|
|
41
41
|
const testSuiteToErrors = new Map()
|
|
42
42
|
const testsToTestStatuses = new Map()
|
|
43
43
|
|
|
44
|
-
const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) ||
|
|
44
|
+
const RUM_FLUSH_WAIT_TIME = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS')) || 500
|
|
45
45
|
|
|
46
46
|
let applyRepeatEachIndex = null
|
|
47
47
|
|
|
@@ -9,6 +9,19 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
|
|
|
9
9
|
static id = 'amqplib'
|
|
10
10
|
static operation = 'consume'
|
|
11
11
|
|
|
12
|
+
start (ctx) {
|
|
13
|
+
if (!this.config.dsmEnabled) return
|
|
14
|
+
const { fields = {}, message, queue } = ctx
|
|
15
|
+
if (!message?.properties?.headers) return
|
|
16
|
+
|
|
17
|
+
const { span } = ctx.currentStore
|
|
18
|
+
const queueName = queue || fields.queue || fields.routingKey
|
|
19
|
+
const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
|
|
20
|
+
this.tracer.decodeDataStreamsContext(message.properties.headers)
|
|
21
|
+
this.tracer
|
|
22
|
+
.setCheckpoint(['direction:in', `topic:${queueName}`, 'type:rabbitmq'], span, payloadSize)
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
bindStart (ctx) {
|
|
13
26
|
const { method, fields = {}, message, queue } = ctx
|
|
14
27
|
|
|
@@ -17,7 +30,7 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
|
|
|
17
30
|
const childOf = extract(this.tracer, message)
|
|
18
31
|
|
|
19
32
|
const queueName = queue || fields.queue || fields.routingKey
|
|
20
|
-
|
|
33
|
+
this.startSpan({
|
|
21
34
|
childOf,
|
|
22
35
|
resource: getResourceName(method, fields),
|
|
23
36
|
type: 'worker',
|
|
@@ -31,15 +44,6 @@ class AmqplibConsumerPlugin extends ConsumerPlugin {
|
|
|
31
44
|
},
|
|
32
45
|
}, ctx)
|
|
33
46
|
|
|
34
|
-
if (
|
|
35
|
-
this.config.dsmEnabled && message?.properties?.headers
|
|
36
|
-
) {
|
|
37
|
-
const payloadSize = getAmqpMessageSize({ headers: message.properties.headers, content: message.content })
|
|
38
|
-
this.tracer.decodeDataStreamsContext(message.properties.headers)
|
|
39
|
-
this.tracer
|
|
40
|
-
.setCheckpoint(['direction:in', `topic:${queueName}`, 'type:rabbitmq'], span, payloadSize)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
47
|
return ctx.currentStore
|
|
44
48
|
}
|
|
45
49
|
}
|
|
@@ -10,8 +10,30 @@ class AmqplibProducerPlugin extends ProducerPlugin {
|
|
|
10
10
|
static id = 'amqplib'
|
|
11
11
|
static operation = 'publish'
|
|
12
12
|
|
|
13
|
+
start (ctx) {
|
|
14
|
+
if (!this.config.dsmEnabled) return
|
|
15
|
+
const { fields, message } = ctx
|
|
16
|
+
const { span } = ctx.currentStore
|
|
17
|
+
|
|
18
|
+
const hasRoutingKey = fields.routingKey != null
|
|
19
|
+
const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
|
|
20
|
+
|
|
21
|
+
// there are two ways to send messages in RabbitMQ:
|
|
22
|
+
// 1. using an exchange and a routing key in which DSM connects via the exchange
|
|
23
|
+
// 2. using an unnamed exchange and a routing key in which DSM connects via the topic
|
|
24
|
+
const exchangeOrTopicTag = hasRoutingKey && !fields.exchange
|
|
25
|
+
? `topic:${fields.routingKey}`
|
|
26
|
+
: `exchange:${fields.exchange}`
|
|
27
|
+
|
|
28
|
+
const dataStreamsContext = this.tracer.setCheckpoint(
|
|
29
|
+
['direction:out', exchangeOrTopicTag, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq'],
|
|
30
|
+
span, payloadSize
|
|
31
|
+
)
|
|
32
|
+
DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
|
|
33
|
+
}
|
|
34
|
+
|
|
13
35
|
bindStart (ctx) {
|
|
14
|
-
const { channel = {}, method, fields
|
|
36
|
+
const { channel = {}, method, fields } = ctx
|
|
15
37
|
|
|
16
38
|
if (method !== 'basic.publish') return
|
|
17
39
|
|
|
@@ -34,24 +56,6 @@ class AmqplibProducerPlugin extends ProducerPlugin {
|
|
|
34
56
|
|
|
35
57
|
this.tracer.inject(span, TEXT_MAP, fields.headers)
|
|
36
58
|
|
|
37
|
-
if (this.config.dsmEnabled) {
|
|
38
|
-
const hasRoutingKey = fields.routingKey != null
|
|
39
|
-
const payloadSize = getAmqpMessageSize({ content: message, headers: fields.headers })
|
|
40
|
-
|
|
41
|
-
// there are two ways to send messages in RabbitMQ:
|
|
42
|
-
// 1. using an exchange and a routing key in which DSM connects via the exchange
|
|
43
|
-
// 2. using an unnamed exchange and a routing key in which DSM connects via the topic
|
|
44
|
-
const exchangeOrTopicTag = hasRoutingKey && !fields.exchange
|
|
45
|
-
? `topic:${fields.routingKey}`
|
|
46
|
-
: `exchange:${fields.exchange}`
|
|
47
|
-
|
|
48
|
-
const dataStreamsContext = this.tracer
|
|
49
|
-
.setCheckpoint(
|
|
50
|
-
['direction:out', exchangeOrTopicTag, `has_routing_key:${hasRoutingKey}`, 'type:rabbitmq']
|
|
51
|
-
, span, payloadSize)
|
|
52
|
-
DsmPathwayCodec.encode(dataStreamsContext, fields.headers)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
59
|
return ctx.currentStore
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -11,17 +11,24 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
|
|
|
11
11
|
ctx.currentStore?.span?.finish()
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
start (ctx) {
|
|
15
|
+
if (!this.config.dsmEnabled) return
|
|
16
|
+
const { span } = ctx.currentStore
|
|
17
|
+
this.setConsumerCheckpoint(span, ctx)
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
bindStart (ctx) {
|
|
15
21
|
const job = ctx.arguments?.[0]
|
|
16
22
|
const queueName = job?.queueName || job?.queue?.name || 'bullmq'
|
|
17
23
|
|
|
18
24
|
let childOf
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
25
|
+
const ddCarrier = this._extractDatadog(job)
|
|
26
|
+
if (ddCarrier) {
|
|
27
|
+
ctx._ddCarrier = ddCarrier
|
|
28
|
+
childOf = this.tracer.extract('text_map', ddCarrier)
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
this.startSpan({
|
|
25
32
|
childOf,
|
|
26
33
|
resource: queueName,
|
|
27
34
|
meta: {
|
|
@@ -33,10 +40,6 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
|
|
|
33
40
|
},
|
|
34
41
|
}, ctx)
|
|
35
42
|
|
|
36
|
-
if (this.config.dsmEnabled) {
|
|
37
|
-
this.setConsumerCheckpoint(span, ctx)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
43
|
return ctx.currentStore
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -47,14 +50,33 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
|
|
|
47
50
|
const queueName = job.queueName || job.queue?.name || 'bullmq'
|
|
48
51
|
const payloadSize = job.data ? getMessageSize(job.data) : 0
|
|
49
52
|
|
|
50
|
-
const
|
|
51
|
-
if (
|
|
52
|
-
this.tracer.decodeDataStreamsContext(
|
|
53
|
+
const ddCarrier = ctx._ddCarrier
|
|
54
|
+
if (ddCarrier) {
|
|
55
|
+
this.tracer.decodeDataStreamsContext(ddCarrier)
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
const edgeTags = ['direction:in', `topic:${queueName}`, 'type:bullmq']
|
|
56
59
|
this.tracer.setCheckpoint(edgeTags, span, payloadSize)
|
|
57
60
|
}
|
|
61
|
+
|
|
62
|
+
_extractDatadog (job) {
|
|
63
|
+
const metadataStr = job?.opts?.telemetry?.metadata
|
|
64
|
+
if (!metadataStr) return
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const metadata = JSON.parse(metadataStr)
|
|
68
|
+
const ddCarrier = metadata._datadog
|
|
69
|
+
if (!ddCarrier) return
|
|
70
|
+
|
|
71
|
+
// Clean up only our _datadog key, preserve other metadata
|
|
72
|
+
delete metadata._datadog
|
|
73
|
+
job.opts.telemetry.metadata = JSON.stringify(metadata)
|
|
74
|
+
|
|
75
|
+
return ddCarrier
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore malformed metadata
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
module.exports = BullmqConsumerPlugin
|