dd-trace 5.36.0 → 5.37.1

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 (67) hide show
  1. package/LICENSE-3rdparty.csv +2 -1
  2. package/index.d.ts +5 -0
  3. package/loader-hook.mjs +0 -4
  4. package/package.json +15 -14
  5. package/packages/datadog-instrumentations/src/cucumber.js +54 -1
  6. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -2
  7. package/packages/datadog-instrumentations/src/helpers/register.js +2 -2
  8. package/packages/datadog-instrumentations/src/jest.js +103 -5
  9. package/packages/datadog-instrumentations/src/mocha/main.js +46 -4
  10. package/packages/datadog-instrumentations/src/mocha/utils.js +35 -2
  11. package/packages/datadog-instrumentations/src/mocha/worker.js +7 -0
  12. package/packages/datadog-instrumentations/src/mysql2.js +3 -3
  13. package/packages/datadog-instrumentations/src/openai.js +8 -0
  14. package/packages/datadog-instrumentations/src/playwright.js +70 -22
  15. package/packages/datadog-instrumentations/src/vitest.js +60 -6
  16. package/packages/datadog-plugin-cucumber/src/index.js +20 -3
  17. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +67 -7
  18. package/packages/datadog-plugin-dd-trace-api/src/index.js +1 -3
  19. package/packages/datadog-plugin-graphql/src/utils.js +8 -1
  20. package/packages/datadog-plugin-jest/src/index.js +12 -2
  21. package/packages/datadog-plugin-mocha/src/index.js +22 -3
  22. package/packages/datadog-plugin-mongodb-core/src/index.js +28 -2
  23. package/packages/datadog-plugin-openai/src/tracing.js +1 -2
  24. package/packages/datadog-plugin-playwright/src/index.js +31 -5
  25. package/packages/datadog-plugin-vitest/src/index.js +25 -1
  26. package/packages/dd-trace/src/appsec/blocked_templates.js +3 -3
  27. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -6
  28. package/packages/dd-trace/src/appsec/iast/analyzers/injection-analyzer.js +15 -1
  29. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +11 -24
  30. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +2 -6
  31. package/packages/dd-trace/src/appsec/iast/analyzers/stored-injection-analyzer.js +11 -0
  32. package/packages/dd-trace/src/appsec/iast/analyzers/template-injection-analyzer.js +2 -6
  33. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +22 -2
  34. package/packages/dd-trace/src/appsec/iast/index.js +2 -0
  35. package/packages/dd-trace/src/appsec/iast/security-controls/index.js +187 -0
  36. package/packages/dd-trace/src/appsec/iast/security-controls/parser.js +96 -0
  37. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +2 -2
  38. package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks-generator.js +1 -1
  39. package/packages/dd-trace/src/appsec/iast/taint-tracking/secure-marks.js +28 -0
  40. package/packages/dd-trace/src/appsec/iast/telemetry/iast-metric.js +5 -0
  41. package/packages/dd-trace/src/appsec/iast/utils.js +24 -0
  42. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +8 -13
  43. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -0
  44. package/packages/dd-trace/src/appsec/rasp/command_injection.js +4 -4
  45. package/packages/dd-trace/src/appsec/rasp/lfi.js +2 -2
  46. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +2 -2
  47. package/packages/dd-trace/src/appsec/rasp/ssrf.js +2 -2
  48. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +1 -1
  49. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +17 -49
  50. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +20 -2
  51. package/packages/dd-trace/src/ci-visibility/quarantined-tests/get-quarantined-tests.js +62 -0
  52. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +5 -2
  53. package/packages/dd-trace/src/config.js +17 -4
  54. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +17 -10
  55. package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +64 -0
  56. package/packages/dd-trace/src/debugger/devtools_client/state.js +38 -10
  57. package/packages/dd-trace/src/iitm.js +2 -2
  58. package/packages/dd-trace/src/llmobs/tagger.js +12 -2
  59. package/packages/dd-trace/src/opentracing/span.js +2 -2
  60. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -7
  61. package/packages/dd-trace/src/plugins/database.js +14 -4
  62. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +1 -3
  63. package/packages/dd-trace/src/plugins/util/test.js +6 -4
  64. package/packages/dd-trace/src/proxy.js +5 -1
  65. package/packages/dd-trace/src/ritm.js +2 -1
  66. package/packages/dd-trace/src/spanleak.js +0 -1
  67. package/packages/memwatch/package.json +0 -9
@@ -26,6 +26,27 @@ const testToStartLine = new WeakMap()
26
26
  const testFileToSuiteAr = new Map()
27
27
  const wrappedFunctions = new WeakSet()
28
28
  const newTests = {}
29
+ const testsQuarantined = new Set()
30
+
31
+ function isQuarantinedTest (test, testsToQuarantine) {
32
+ const testSuite = getTestSuitePath(test.file, process.cwd())
33
+ const testName = test.fullTitle()
34
+
35
+ const isQuarantined = (testsToQuarantine
36
+ .mocha
37
+ ?.suites
38
+ ?.[testSuite]
39
+ ?.tests
40
+ ?.[testName]
41
+ ?.properties
42
+ ?.quarantined) ?? false
43
+
44
+ if (isQuarantined) {
45
+ testsQuarantined.add(test)
46
+ }
47
+
48
+ return isQuarantined
49
+ }
29
50
 
30
51
  function isNewTest (test, knownTests) {
31
52
  const testSuite = getTestSuitePath(test.file, process.cwd())
@@ -171,7 +192,8 @@ function getOnTestHandler (isMain) {
171
192
  file: testSuiteAbsolutePath,
172
193
  title,
173
194
  _ddIsNew: isNew,
174
- _ddIsEfdRetry: isEfdRetry
195
+ _ddIsEfdRetry: isEfdRetry,
196
+ _ddIsQuarantined: isQuarantined
175
197
  } = test
176
198
 
177
199
  const testInfo = {
@@ -187,6 +209,7 @@ function getOnTestHandler (isMain) {
187
209
 
188
210
  testInfo.isNew = isNew
189
211
  testInfo.isEfdRetry = isEfdRetry
212
+ testInfo.isQuarantined = isQuarantined
190
213
  // We want to store the result of the new tests
191
214
  if (isNew) {
192
215
  const testFullName = getTestFullName(test)
@@ -360,6 +383,15 @@ function getRunTestsWrapper (runTests, config) {
360
383
  }
361
384
  })
362
385
  }
386
+
387
+ if (config.isQuarantinedTestsEnabled) {
388
+ suite.tests.forEach(test => {
389
+ if (isQuarantinedTest(test, config.quarantinedTests)) {
390
+ test._ddIsQuarantined = true
391
+ }
392
+ })
393
+ }
394
+
363
395
  return runTests.apply(this, arguments)
364
396
  }
365
397
  }
@@ -384,5 +416,6 @@ module.exports = {
384
416
  getOnPendingHandler,
385
417
  testFileToSuiteAr,
386
418
  getRunTestsWrapper,
387
- newTests
419
+ newTests,
420
+ testsQuarantined
388
421
  }
@@ -33,6 +33,13 @@ addHook({
33
33
  delete this.options._ddIsEfdEnabled
34
34
  delete this.options._ddKnownTests
35
35
  delete this.options._ddEfdNumRetries
36
+ delete this.options._ddQuarantinedTests
37
+ }
38
+ if (this.options._ddIsQuarantinedEnabled) {
39
+ config.isQuarantinedEnabled = true
40
+ config.quarantinedTests = this.options._ddQuarantinedTests
41
+ delete this.options._ddIsQuarantinedEnabled
42
+ delete this.options._ddQuarantinedTests
36
43
  }
37
44
  return run.apply(this, arguments)
38
45
  })
@@ -6,14 +6,14 @@ const {
6
6
  AsyncResource
7
7
  } = require('./helpers/instrument')
8
8
  const shimmer = require('../../datadog-shimmer')
9
- const semver = require('semver')
9
+ const satisfies = require('semifies')
10
10
 
11
11
  function wrapConnection (Connection, version) {
12
12
  const startCh = channel('apm:mysql2:query:start')
13
13
  const finishCh = channel('apm:mysql2:query:finish')
14
14
  const errorCh = channel('apm:mysql2:query:error')
15
15
  const startOuterQueryCh = channel('datadog:mysql2:outerquery:start')
16
- const shouldEmitEndAfterQueryAbort = semver.intersects(version, '>=1.3.3')
16
+ const shouldEmitEndAfterQueryAbort = satisfies(version, '>=1.3.3')
17
17
 
18
18
  shimmer.wrap(Connection.prototype, 'addCommand', addCommand => function (cmd) {
19
19
  if (!startCh.hasSubscribers) return addCommand.apply(this, arguments)
@@ -154,7 +154,7 @@ function wrapConnection (Connection, version) {
154
154
  }
155
155
  function wrapPool (Pool, version) {
156
156
  const startOuterQueryCh = channel('datadog:mysql2:outerquery:start')
157
- const shouldEmitEndAfterQueryAbort = semver.intersects(version, '>=1.3.3')
157
+ const shouldEmitEndAfterQueryAbort = satisfies(version, '>=1.3.3')
158
158
 
159
159
  shimmer.wrap(Pool.prototype, 'query', query => function (sql, values, cb) {
160
160
  if (!startOuterQueryCh.hasSubscribers) return query.apply(this, arguments)
@@ -97,6 +97,14 @@ const V4_PACKAGE_SHIMS = [
97
97
  targetClass: 'Translations',
98
98
  baseResource: 'audio.translations',
99
99
  methods: ['create']
100
+ },
101
+ {
102
+ file: 'resources/chat/completions/completions.js',
103
+ targetClass: 'Completions',
104
+ baseResource: 'chat.completions',
105
+ methods: ['create'],
106
+ streamedResponse: true,
107
+ versions: ['>=4.85.0']
100
108
  }
101
109
  ]
102
110
 
@@ -1,4 +1,4 @@
1
- const semver = require('semver')
1
+ const satisfies = require('semifies')
2
2
 
3
3
  const { addHook, channel, AsyncResource } = require('./helpers/instrument')
4
4
  const shimmer = require('../../datadog-shimmer')
@@ -13,6 +13,7 @@ const testSessionFinishCh = channel('ci:playwright:session:finish')
13
13
 
14
14
  const libraryConfigurationCh = channel('ci:playwright:library-configuration')
15
15
  const knownTestsCh = channel('ci:playwright:known-tests')
16
+ const quarantinedTestsCh = channel('ci:playwright:quarantined-tests')
16
17
 
17
18
  const testSuiteStartCh = channel('ci:playwright:test-suite:start')
18
19
  const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
@@ -41,8 +42,24 @@ let earlyFlakeDetectionNumRetries = 0
41
42
  let isFlakyTestRetriesEnabled = false
42
43
  let flakyTestRetriesCount = 0
43
44
  let knownTests = {}
45
+ let isQuarantinedTestsEnabled = false
46
+ let quarantinedTests = {}
44
47
  let rootDir = ''
45
- const MINIMUM_SUPPORTED_VERSION_EFD = '1.38.0'
48
+ const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0'
49
+
50
+ function isQuarantineTest (test) {
51
+ const testName = getTestFullname(test)
52
+ const testSuite = getTestSuitePath(test._requireFile, rootDir)
53
+
54
+ return quarantinedTests
55
+ ?.playwright
56
+ ?.suites
57
+ ?.[testSuite]
58
+ ?.tests
59
+ ?.[testName]
60
+ ?.properties
61
+ ?.quarantined
62
+ }
46
63
 
47
64
  function isNewTest (test) {
48
65
  const testSuite = getTestSuitePath(test._requireFile, rootDir)
@@ -296,6 +313,7 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
296
313
  error,
297
314
  extraTags: annotationTags,
298
315
  isNew: test._ddIsNew,
316
+ isQuarantined: test._ddIsQuarantined,
299
317
  isEfdRetry: test._ddIsEfdRetry
300
318
  })
301
319
  })
@@ -424,14 +442,16 @@ function runnerHook (runnerExport, playwrightVersion) {
424
442
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
425
443
  isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
426
444
  flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
445
+ isQuarantinedTestsEnabled = libraryConfig.isQuarantinedTestsEnabled
427
446
  }
428
447
  } catch (e) {
429
448
  isEarlyFlakeDetectionEnabled = false
430
449
  isKnownTestsEnabled = false
450
+ isQuarantinedTestsEnabled = false
431
451
  log.error('Playwright session start error', e)
432
452
  }
433
453
 
434
- if (isKnownTestsEnabled && semver.gte(playwrightVersion, MINIMUM_SUPPORTED_VERSION_EFD)) {
454
+ if (isKnownTestsEnabled && satisfies(playwrightVersion, MINIMUM_SUPPORTED_VERSION_RANGE_EFD)) {
435
455
  try {
436
456
  const { err, knownTests: receivedKnownTests } = await getChannelPromise(knownTestsCh)
437
457
  if (!err) {
@@ -447,6 +467,20 @@ function runnerHook (runnerExport, playwrightVersion) {
447
467
  }
448
468
  }
449
469
 
470
+ if (isQuarantinedTestsEnabled && satisfies(playwrightVersion, MINIMUM_SUPPORTED_VERSION_RANGE_EFD)) {
471
+ try {
472
+ const { err, quarantinedTests: receivedQuarantinedTests } = await getChannelPromise(quarantinedTestsCh)
473
+ if (!err) {
474
+ quarantinedTests = receivedQuarantinedTests
475
+ } else {
476
+ isQuarantinedTestsEnabled = false
477
+ }
478
+ } catch (err) {
479
+ isQuarantinedTestsEnabled = false
480
+ log.error('Playwright quarantined tests error', err)
481
+ }
482
+ }
483
+
450
484
  const projects = getProjectsFromRunner(this)
451
485
 
452
486
  if (isFlakyTestRetriesEnabled && flakyTestRetriesCount > 0) {
@@ -479,6 +513,7 @@ function runnerHook (runnerExport, playwrightVersion) {
479
513
  testSessionFinishCh.publish({
480
514
  status: STATUS_TO_TEST_STATUS[sessionStatus],
481
515
  isEarlyFlakeDetectionEnabled,
516
+ isQuarantinedTestsEnabled,
482
517
  onDone
483
518
  })
484
519
  })
@@ -487,6 +522,8 @@ function runnerHook (runnerExport, playwrightVersion) {
487
522
  startedSuites = []
488
523
  remainingTestsByFile = {}
489
524
 
525
+ // TODO: we can trick playwright into thinking the session passed by returning
526
+ // 'passed' here. We might be able to use this for both EFD and Quarantined tests.
490
527
  return runAllTestsReturn
491
528
  })
492
529
 
@@ -540,7 +577,7 @@ addHook({
540
577
  addHook({
541
578
  name: 'playwright',
542
579
  file: 'lib/common/suiteUtils.js',
543
- versions: [`>=${MINIMUM_SUPPORTED_VERSION_EFD}`]
580
+ versions: [MINIMUM_SUPPORTED_VERSION_RANGE_EFD]
544
581
  }, suiteUtilsPackage => {
545
582
  // We grab `applyRepeatEachIndex` to use it later
546
583
  // `applyRepeatEachIndex` needs to be applied to a cloned suite
@@ -552,31 +589,42 @@ addHook({
552
589
  addHook({
553
590
  name: 'playwright',
554
591
  file: 'lib/runner/loadUtils.js',
555
- versions: [`>=${MINIMUM_SUPPORTED_VERSION_EFD}`]
592
+ versions: [MINIMUM_SUPPORTED_VERSION_RANGE_EFD]
556
593
  }, (loadUtilsPackage) => {
557
594
  const oldCreateRootSuite = loadUtilsPackage.createRootSuite
558
595
 
559
596
  async function newCreateRootSuite () {
597
+ if (!isKnownTestsEnabled && !isQuarantinedTestsEnabled) {
598
+ return oldCreateRootSuite.apply(this, arguments)
599
+ }
560
600
  const rootSuite = await oldCreateRootSuite.apply(this, arguments)
561
- if (!isKnownTestsEnabled) {
562
- return rootSuite
601
+
602
+ const allTests = rootSuite.allTests()
603
+
604
+ if (isQuarantinedTestsEnabled) {
605
+ const testsToBeIgnored = allTests.filter(isQuarantineTest)
606
+ testsToBeIgnored.forEach(test => {
607
+ test._ddIsQuarantined = true
608
+ test.expectedStatus = 'skipped'
609
+ })
563
610
  }
564
- const newTests = rootSuite
565
- .allTests()
566
- .filter(isNewTest)
567
-
568
- newTests.forEach(newTest => {
569
- newTest._ddIsNew = true
570
- if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped') {
571
- const fileSuite = getSuiteType(newTest, 'file')
572
- const projectSuite = getSuiteType(newTest, 'project')
573
- for (let repeatEachIndex = 0; repeatEachIndex < earlyFlakeDetectionNumRetries; repeatEachIndex++) {
574
- const copyFileSuite = deepCloneSuite(fileSuite, isNewTest)
575
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
576
- projectSuite._addSuite(copyFileSuite)
611
+
612
+ if (isKnownTestsEnabled) {
613
+ const newTests = allTests.filter(isNewTest)
614
+
615
+ newTests.forEach(newTest => {
616
+ newTest._ddIsNew = true
617
+ if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped') {
618
+ const fileSuite = getSuiteType(newTest, 'file')
619
+ const projectSuite = getSuiteType(newTest, 'project')
620
+ for (let repeatEachIndex = 0; repeatEachIndex < earlyFlakeDetectionNumRetries; repeatEachIndex++) {
621
+ const copyFileSuite = deepCloneSuite(fileSuite, isNewTest)
622
+ applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
623
+ projectSuite._addSuite(copyFileSuite)
624
+ }
577
625
  }
578
- }
579
- })
626
+ })
627
+ }
580
628
 
581
629
  return rootSuite
582
630
  }
@@ -9,6 +9,7 @@ const testPassCh = channel('ci:vitest:test:pass')
9
9
  const testErrorCh = channel('ci:vitest:test:error')
10
10
  const testSkipCh = channel('ci:vitest:test:skip')
11
11
  const isNewTestCh = channel('ci:vitest:test:is-new')
12
+ const isQuarantinedCh = channel('ci:vitest:test:is-quarantined')
12
13
 
13
14
  // test suite hooks
14
15
  const testSuiteStartCh = channel('ci:vitest:test-suite:start')
@@ -21,10 +22,12 @@ const testSessionFinishCh = channel('ci:vitest:session:finish')
21
22
  const libraryConfigurationCh = channel('ci:vitest:library-configuration')
22
23
  const knownTestsCh = channel('ci:vitest:known-tests')
23
24
  const isEarlyFlakeDetectionFaultyCh = channel('ci:vitest:is-early-flake-detection-faulty')
25
+ const quarantinedTestsCh = channel('ci:vitest:quarantined-tests')
24
26
 
25
27
  const taskToAsync = new WeakMap()
26
28
  const taskToStatuses = new WeakMap()
27
29
  const newTasks = new WeakSet()
30
+ const quarantinedTasks = new WeakSet()
28
31
  let isRetryReasonEfd = false
29
32
  const switchedStatuses = new WeakSet()
30
33
  const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
@@ -46,7 +49,9 @@ function getProvidedContext () {
46
49
  _ddIsDiEnabled,
47
50
  _ddKnownTests: knownTests,
48
51
  _ddEarlyFlakeDetectionNumRetries: numRepeats,
49
- _ddIsKnownTestsEnabled: isKnownTestsEnabled
52
+ _ddIsKnownTestsEnabled: isKnownTestsEnabled,
53
+ _ddIsQuarantinedTestsEnabled: isQuarantinedTestsEnabled,
54
+ _ddQuarantinedTests: quarantinedTests
50
55
  } = globalThis.__vitest_worker__.providedContext
51
56
 
52
57
  return {
@@ -54,7 +59,9 @@ function getProvidedContext () {
54
59
  isEarlyFlakeDetectionEnabled: _ddIsEarlyFlakeDetectionEnabled,
55
60
  knownTests,
56
61
  numRepeats,
57
- isKnownTestsEnabled
62
+ isKnownTestsEnabled,
63
+ isQuarantinedTestsEnabled,
64
+ quarantinedTests
58
65
  }
59
66
  } catch (e) {
60
67
  log.error('Vitest workers could not parse provided context, so some features will not work.')
@@ -63,7 +70,9 @@ function getProvidedContext () {
63
70
  isEarlyFlakeDetectionEnabled: false,
64
71
  knownTests: {},
65
72
  numRepeats: 0,
66
- isKnownTestsEnabled: false
73
+ isKnownTestsEnabled: false,
74
+ isQuarantinedTestsEnabled: false,
75
+ quarantinedTests: {}
67
76
  }
68
77
  }
69
78
  }
@@ -158,8 +167,10 @@ function getSortWrapper (sort) {
158
167
  let earlyFlakeDetectionNumRetries = 0
159
168
  let isEarlyFlakeDetectionFaulty = false
160
169
  let isKnownTestsEnabled = false
170
+ let isQuarantinedTestsEnabled = false
161
171
  let isDiEnabled = false
162
172
  let knownTests = {}
173
+ let quarantinedTests = {}
163
174
 
164
175
  try {
165
176
  const { err, libraryConfig } = await getChannelPromise(libraryConfigurationCh)
@@ -170,6 +181,7 @@ function getSortWrapper (sort) {
170
181
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
171
182
  isDiEnabled = libraryConfig.isDiEnabled
172
183
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
184
+ isQuarantinedTestsEnabled = libraryConfig.isQuarantinedTestsEnabled
173
185
  }
174
186
  } catch (e) {
175
187
  isFlakyTestRetriesEnabled = false
@@ -229,6 +241,23 @@ function getSortWrapper (sort) {
229
241
  }
230
242
  }
231
243
 
244
+ if (isQuarantinedTestsEnabled) {
245
+ const { err, quarantinedTests: receivedQuarantinedTests } = await getChannelPromise(quarantinedTestsCh)
246
+ if (!err) {
247
+ quarantinedTests = receivedQuarantinedTests
248
+ try {
249
+ const workspaceProject = this.ctx.getCoreWorkspaceProject()
250
+ workspaceProject._provided._ddIsQuarantinedTestsEnabled = isQuarantinedTestsEnabled
251
+ workspaceProject._provided._ddQuarantinedTests = quarantinedTests
252
+ } catch (e) {
253
+ log.warn('Could not send quarantined tests to workers so Quarantine will not work.')
254
+ }
255
+ } else {
256
+ isQuarantinedTestsEnabled = false
257
+ log.error('Could not get quarantined tests.')
258
+ }
259
+ }
260
+
232
261
  let testCodeCoverageLinesTotal
233
262
 
234
263
  if (this.ctx.coverageProvider?.generateCoverage) {
@@ -263,6 +292,7 @@ function getSortWrapper (sort) {
263
292
  error,
264
293
  isEarlyFlakeDetectionEnabled,
265
294
  isEarlyFlakeDetectionFaulty,
295
+ isQuarantinedTestsEnabled,
266
296
  onFinish
267
297
  })
268
298
  })
@@ -332,7 +362,7 @@ addHook({
332
362
 
333
363
  // `onAfterRunTask` is run after all repetitions or attempts are run
334
364
  shimmer.wrap(VitestTestRunner.prototype, 'onAfterRunTask', onAfterRunTask => async function (task) {
335
- const { isEarlyFlakeDetectionEnabled } = getProvidedContext()
365
+ const { isEarlyFlakeDetectionEnabled, isQuarantinedTestsEnabled } = getProvidedContext()
336
366
 
337
367
  if (isEarlyFlakeDetectionEnabled && taskToStatuses.has(task)) {
338
368
  const statuses = taskToStatuses.get(task)
@@ -345,6 +375,12 @@ addHook({
345
375
  }
346
376
  }
347
377
 
378
+ if (isQuarantinedTestsEnabled) {
379
+ if (quarantinedTasks.has(task)) {
380
+ task.result.state = 'pass'
381
+ }
382
+ }
383
+
348
384
  return onAfterRunTask.apply(this, arguments)
349
385
  })
350
386
 
@@ -356,17 +392,34 @@ addHook({
356
392
  }
357
393
  const testName = getTestName(task)
358
394
  let isNew = false
395
+ let isQuarantined = false
359
396
 
360
397
  const {
361
398
  isKnownTestsEnabled,
362
399
  isEarlyFlakeDetectionEnabled,
363
- isDiEnabled
400
+ isDiEnabled,
401
+ isQuarantinedTestsEnabled,
402
+ quarantinedTests
364
403
  } = getProvidedContext()
365
404
 
366
405
  if (isKnownTestsEnabled) {
367
406
  isNew = newTasks.has(task)
368
407
  }
369
408
 
409
+ if (isQuarantinedTestsEnabled) {
410
+ isQuarantinedCh.publish({
411
+ quarantinedTests,
412
+ testSuiteAbsolutePath: task.file.filepath,
413
+ testName,
414
+ onDone: (isTestQuarantined) => {
415
+ isQuarantined = isTestQuarantined
416
+ if (isTestQuarantined) {
417
+ quarantinedTasks.add(task)
418
+ }
419
+ }
420
+ })
421
+ }
422
+
370
423
  const { retry: numAttempt, repeats: numRepetition } = retryInfo
371
424
 
372
425
  // We finish the previous test here because we know it has failed already
@@ -448,7 +501,8 @@ addHook({
448
501
  isRetry: numAttempt > 0 || numRepetition > 0,
449
502
  isRetryReasonEfd,
450
503
  isNew,
451
- mightHitProbe: isDiEnabled && numAttempt > 0
504
+ mightHitProbe: isDiEnabled && numAttempt > 0,
505
+ isQuarantined
452
506
  })
453
507
  })
454
508
  return onBeforeTryTask.apply(this, arguments)
@@ -27,7 +27,9 @@ const {
27
27
  TEST_MODULE_ID,
28
28
  TEST_SUITE,
29
29
  CUCUMBER_IS_PARALLEL,
30
- TEST_RETRY_REASON
30
+ TEST_RETRY_REASON,
31
+ TEST_MANAGEMENT_ENABLED,
32
+ TEST_MANAGEMENT_IS_QUARANTINED
31
33
  } = require('../../dd-trace/src/plugins/util/test')
32
34
  const { RESOURCE_NAME } = require('../../../ext/tags')
33
35
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -47,6 +49,7 @@ const {
47
49
  const id = require('../../dd-trace/src/id')
48
50
 
49
51
  const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
52
+ const BREAKPOINT_SET_GRACE_PERIOD_MS = 200
50
53
  const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID
51
54
 
52
55
  function getTestSuiteTags (testSuiteSpan) {
@@ -83,6 +86,7 @@ class CucumberPlugin extends CiPlugin {
83
86
  hasForcedToRunSuites,
84
87
  isEarlyFlakeDetectionEnabled,
85
88
  isEarlyFlakeDetectionFaulty,
89
+ isQuarantinedTestsEnabled,
86
90
  isParallel
87
91
  }) => {
88
92
  const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.libraryConfig || {}
@@ -109,6 +113,9 @@ class CucumberPlugin extends CiPlugin {
109
113
  if (isParallel) {
110
114
  this.testSessionSpan.setTag(CUCUMBER_IS_PARALLEL, 'true')
111
115
  }
116
+ if (isQuarantinedTestsEnabled) {
117
+ this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
118
+ }
112
119
 
113
120
  this.testSessionSpan.setTag(TEST_STATUS, status)
114
121
  this.testModuleSpan.setTag(TEST_STATUS, status)
@@ -251,7 +258,12 @@ class CucumberPlugin extends CiPlugin {
251
258
  const { file, line, stackIndex } = probeInformation
252
259
  this.runningTestProbe = { file, line }
253
260
  this.testErrorStackIndex = stackIndex
254
- // TODO: we're not waiting for setProbePromise to be resolved, so there might be race conditions
261
+ const waitUntil = Date.now() + BREAKPOINT_SET_GRACE_PERIOD_MS
262
+ while (Date.now() < waitUntil) {
263
+ // TODO: To avoid a race condition, we should wait until `probeInformation.setProbePromise` has resolved.
264
+ // However, Cucumber doesn't have a mechanism for waiting asyncrounously here, so for now, we'll have to
265
+ // fall back to a fixed syncronous delay.
266
+ }
255
267
  }
256
268
  }
257
269
  span.setTag(TEST_STATUS, 'fail')
@@ -311,7 +323,8 @@ class CucumberPlugin extends CiPlugin {
311
323
  errorMessage,
312
324
  isNew,
313
325
  isEfdRetry,
314
- isFlakyRetry
326
+ isFlakyRetry,
327
+ isQuarantined
315
328
  }) => {
316
329
  const span = storage('legacy').getStore().span
317
330
  const statusTag = isStep ? 'step.status' : TEST_STATUS
@@ -340,6 +353,10 @@ class CucumberPlugin extends CiPlugin {
340
353
  span.setTag(TEST_IS_RETRY, 'true')
341
354
  }
342
355
 
356
+ if (isQuarantined) {
357
+ span.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
358
+ }
359
+
343
360
  span.finish()
344
361
  if (!isStep) {
345
362
  const spanTags = span.context()._tags
@@ -33,7 +33,9 @@ const {
33
33
  TEST_SESSION_NAME,
34
34
  TEST_LEVEL_EVENT_TYPES,
35
35
  TEST_RETRY_REASON,
36
- DD_TEST_IS_USER_PROVIDED_SERVICE
36
+ DD_TEST_IS_USER_PROVIDED_SERVICE,
37
+ TEST_MANAGEMENT_IS_QUARANTINED,
38
+ TEST_MANAGEMENT_ENABLED
37
39
  } = require('../../dd-trace/src/plugins/util/test')
38
40
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
39
41
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -152,6 +154,20 @@ function getKnownTests (tracer, testConfiguration) {
152
154
  })
153
155
  }
154
156
 
157
+ function getQuarantinedTests (tracer, testConfiguration) {
158
+ return new Promise(resolve => {
159
+ if (!tracer._tracer._exporter?.getQuarantinedTests) {
160
+ return resolve({ err: new Error('Test Optimization was not initialized correctly') })
161
+ }
162
+ tracer._tracer._exporter.getQuarantinedTests(testConfiguration, (err, quarantinedTests) => {
163
+ resolve({
164
+ err,
165
+ quarantinedTests
166
+ })
167
+ })
168
+ })
169
+ }
170
+
155
171
  function getSuiteStatus (suiteStats) {
156
172
  if (!suiteStats) {
157
173
  return 'skip'
@@ -240,7 +256,8 @@ class CypressPlugin {
240
256
  earlyFlakeDetectionNumRetries,
241
257
  isFlakyTestRetriesEnabled,
242
258
  flakyTestRetriesCount,
243
- isKnownTestsEnabled
259
+ isKnownTestsEnabled,
260
+ isQuarantinedTestsEnabled
244
261
  }
245
262
  } = libraryConfigurationResponse
246
263
  this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
@@ -251,12 +268,24 @@ class CypressPlugin {
251
268
  if (isFlakyTestRetriesEnabled) {
252
269
  this.cypressConfig.retries.runMode = flakyTestRetriesCount
253
270
  }
271
+ this.isQuarantinedTestsEnabled = isQuarantinedTestsEnabled
254
272
  }
255
273
  return this.cypressConfig
256
274
  })
257
275
  return this.libraryConfigurationPromise
258
276
  }
259
277
 
278
+ getIsQuarantinedTest (testSuite, testName) {
279
+ return this.quarantinedTests
280
+ ?.cypress
281
+ ?.suites
282
+ ?.[testSuite]
283
+ ?.tests
284
+ ?.[testName]
285
+ ?.properties
286
+ ?.quarantined
287
+ }
288
+
260
289
  getTestSuiteSpan ({ testSuite, testSuiteAbsolutePath }) {
261
290
  const testSuiteSpanMetadata =
262
291
  getTestSuiteCommonTags(this.command, this.frameworkVersion, testSuite, TEST_FRAMEWORK_NAME)
@@ -351,10 +380,6 @@ class CypressPlugin {
351
380
  })
352
381
  }
353
382
 
354
- isNewTest (testName, testSuite) {
355
- return !this.knownTestsByTestSuite?.[testSuite]?.includes(testName)
356
- }
357
-
358
383
  async beforeRun (details) {
359
384
  // We need to make sure that the plugin is initialized before running the tests
360
385
  // This is for the case where the user has not returned the promise from the init function
@@ -393,6 +418,19 @@ class CypressPlugin {
393
418
  }
394
419
  }
395
420
 
421
+ if (this.isQuarantinedTestsEnabled) {
422
+ const quarantinedTestsResponse = await getQuarantinedTests(
423
+ this.tracer,
424
+ this.testConfiguration
425
+ )
426
+ if (quarantinedTestsResponse.err) {
427
+ log.error('Cypress quarantined tests response error', quarantinedTestsResponse.err)
428
+ this.isQuarantinedTestsEnabled = false
429
+ } else {
430
+ this.quarantinedTests = quarantinedTestsResponse.quarantinedTests
431
+ }
432
+ }
433
+
396
434
  // `details.specs` are test files
397
435
  details.specs?.forEach(({ absolute, relative }) => {
398
436
  const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute })
@@ -471,6 +509,10 @@ class CypressPlugin {
471
509
  }
472
510
  )
473
511
 
512
+ if (this.isQuarantinedTestsEnabled) {
513
+ this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
514
+ }
515
+
474
516
  this.testModuleSpan.finish()
475
517
  this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
476
518
  this.testSessionSpan.finish()
@@ -545,6 +587,13 @@ class CypressPlugin {
545
587
  if (this.itrCorrelationId) {
546
588
  skippedTestSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
547
589
  }
590
+
591
+ const isQuarantined = this.getIsQuarantinedTest(spec.relative, cypressTestName)
592
+
593
+ if (isQuarantined) {
594
+ skippedTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
595
+ }
596
+
548
597
  skippedTestSpan.finish()
549
598
  })
550
599
 
@@ -648,6 +697,7 @@ class CypressPlugin {
648
697
  })
649
698
  const isUnskippable = this.unskippableSuites.includes(testSuite)
650
699
  const isForcedToRun = shouldSkip && isUnskippable
700
+ const isQuarantined = this.getIsQuarantinedTest(testSuite, testName)
651
701
 
652
702
  // skip test
653
703
  if (shouldSkip && !isUnskippable) {
@@ -656,6 +706,12 @@ class CypressPlugin {
656
706
  return { shouldSkip: true }
657
707
  }
658
708
 
709
+ // TODO: I haven't found a way to trick cypress into ignoring a test
710
+ // The way we'll implement quarantine in cypress is by skipping the test altogether
711
+ if (isQuarantined) {
712
+ return { shouldSkip: true }
713
+ }
714
+
659
715
  if (!this.activeTestSpan) {
660
716
  this.activeTestSpan = this.getTestSpan({
661
717
  testName,
@@ -681,7 +737,8 @@ class CypressPlugin {
681
737
  testSuiteAbsolutePath,
682
738
  testName,
683
739
  isNew,
684
- isEfdRetry
740
+ isEfdRetry,
741
+ isQuarantined
685
742
  } = test
686
743
  if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
687
744
  const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
@@ -720,6 +777,9 @@ class CypressPlugin {
720
777
  this.activeTestSpan.setTag(TEST_RETRY_REASON, 'efd')
721
778
  }
722
779
  }
780
+ if (isQuarantined) {
781
+ this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_QUARANTINED, 'true')
782
+ }
723
783
  const finishedTest = {
724
784
  testName,
725
785
  testStatus,