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.
Files changed (77) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +225 -4
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  15. package/packages/datadog-instrumentations/src/jest.js +76 -12
  16. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  17. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  18. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  19. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  20. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  21. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  22. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  23. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +26 -0
  24. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  25. package/packages/datadog-plugin-jest/src/index.js +12 -2
  26. package/packages/datadog-plugin-jest/src/util.js +2 -1
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  28. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  29. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  30. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  31. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  32. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  33. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  34. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  36. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  37. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  38. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  40. package/packages/dd-trace/src/azure_metadata.js +0 -2
  41. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  43. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  44. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  45. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  46. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  47. package/packages/dd-trace/src/config/defaults.js +148 -197
  48. package/packages/dd-trace/src/config/helper.js +43 -1
  49. package/packages/dd-trace/src/config/index.js +36 -14
  50. package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
  51. package/packages/dd-trace/src/constants.js +0 -2
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  53. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  54. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  55. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  56. package/packages/dd-trace/src/exporter.js +2 -0
  57. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  58. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  59. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  60. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  61. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  62. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  63. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  64. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  65. package/packages/dd-trace/src/opentracing/span.js +6 -4
  66. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  67. package/packages/dd-trace/src/plugins/database.js +15 -2
  68. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  69. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  70. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  71. package/packages/dd-trace/src/proxy.js +4 -0
  72. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  73. package/packages/dd-trace/src/startup-log.js +1 -1
  74. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  75. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  76. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  77. 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 ({ format }, node) {
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 = format === 'module'
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, { module: format === 'module' }).body[0])
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: 'ArrowFunctionExpression',
59
+ type: 'FunctionExpression',
60
60
  params: node.params,
61
61
  body: node.body,
62
- async: operator === 'tracePromise',
62
+ async: node.async,
63
63
  expression: false,
64
- generator: false,
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
- earlyFlakeDetectionNumRetries = this.testEnvironmentOptions._ddEarlyFlakeDetectionNumRetries
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
- for (const hook of event.test.parent.hooks) {
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
- this.retryTest({
541
- jestEvent: event,
542
- retryCount: earlyFlakeDetectionNumRetries,
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
- if (testStatuses.length === earlyFlakeDetectionNumRetries + 1 &&
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 isLastEfdRetry = isEfdRetry && numberOfExecutedRetries >= (earlyFlakeDetectionNumRetries + 1)
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
- _ddEarlyFlakeDetectionNumRetries,
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, commitsFromEvent)
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')) || 1000
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
- const span = this.startSpan({
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, message } = ctx
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 datadogContext = job?.data?._datadog
20
- if (datadogContext) {
21
- childOf = this.tracer.extract('text_map', datadogContext)
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
- const span = this.startSpan({
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 datadogContext = job.data?._datadog
51
- if (datadogContext) {
52
- this.tracer.decodeDataStreamsContext(datadogContext)
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