dd-trace 5.45.0 → 5.47.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 (86) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/ci/init.js +8 -0
  3. package/ext/exporters.d.ts +2 -1
  4. package/ext/exporters.js +2 -1
  5. package/package.json +8 -9
  6. package/packages/datadog-instrumentations/orchestrion.yml +52 -0
  7. package/packages/datadog-instrumentations/src/cucumber.js +2 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/register.js +41 -1
  10. package/packages/datadog-instrumentations/src/jest.js +11 -2
  11. package/packages/datadog-instrumentations/src/langchain.js +49 -53
  12. package/packages/datadog-instrumentations/src/mariadb.js +19 -0
  13. package/packages/datadog-instrumentations/src/mocha/main.js +1 -1
  14. package/packages/datadog-instrumentations/src/mocha/utils.js +11 -3
  15. package/packages/datadog-instrumentations/src/orchestrion-config/index.js +5 -0
  16. package/packages/datadog-instrumentations/src/playwright.js +333 -46
  17. package/packages/datadog-instrumentations/src/router.js +1 -7
  18. package/packages/datadog-instrumentations/src/vitest.js +11 -3
  19. package/packages/datadog-plugin-cucumber/src/index.js +11 -4
  20. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +17 -5
  21. package/packages/datadog-plugin-jest/src/index.js +11 -4
  22. package/packages/datadog-plugin-langchain/src/index.js +18 -12
  23. package/packages/datadog-plugin-langchain/src/tracing.js +66 -6
  24. package/packages/datadog-plugin-mocha/src/index.js +17 -5
  25. package/packages/datadog-plugin-mongodb-core/src/index.js +24 -0
  26. package/packages/datadog-plugin-playwright/src/index.js +124 -10
  27. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  28. package/packages/datadog-shimmer/src/shimmer.js +3 -42
  29. package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +39 -15
  30. package/packages/dd-trace/src/appsec/iast/taint-tracking/filter.js +3 -3
  31. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +0 -3
  32. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-esm.mjs +25 -12
  33. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter-telemetry.js +3 -32
  34. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +99 -57
  35. package/packages/dd-trace/src/appsec/rasp/command_injection.js +1 -1
  36. package/packages/dd-trace/src/appsec/rasp/index.js +4 -2
  37. package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
  38. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +1 -1
  39. package/packages/dd-trace/src/appsec/rasp/ssrf.js +1 -1
  40. package/packages/dd-trace/src/appsec/rasp/utils.js +12 -7
  41. package/packages/dd-trace/src/appsec/recommended.json +256 -84
  42. package/packages/dd-trace/src/appsec/reporter.js +6 -4
  43. package/packages/dd-trace/src/appsec/telemetry/index.js +27 -3
  44. package/packages/dd-trace/src/appsec/telemetry/rasp.js +70 -6
  45. package/packages/dd-trace/src/appsec/telemetry/waf.js +0 -30
  46. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -0
  47. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +8 -3
  48. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +6 -4
  49. package/packages/dd-trace/src/config.js +9 -0
  50. package/packages/dd-trace/src/constants.js +1 -0
  51. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +102 -22
  52. package/packages/dd-trace/src/debugger/devtools_client/condition.js +263 -0
  53. package/packages/dd-trace/src/debugger/devtools_client/index.js +69 -36
  54. package/packages/dd-trace/src/debugger/devtools_client/lock.js +8 -0
  55. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +1 -7
  56. package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -2
  57. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +15 -10
  58. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +3 -3
  59. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +69 -62
  60. package/packages/dd-trace/src/debugger/devtools_client/state.js +3 -2
  61. package/packages/dd-trace/src/debugger/index.js +3 -0
  62. package/packages/dd-trace/src/encode/0.4.js +24 -17
  63. package/packages/dd-trace/src/exporter.js +1 -0
  64. package/packages/dd-trace/src/exporters/common/docker.js +37 -7
  65. package/packages/dd-trace/src/exporters/common/request.js +1 -4
  66. package/packages/dd-trace/src/format.js +58 -60
  67. package/packages/dd-trace/src/llmobs/plugins/base.js +2 -2
  68. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +62 -3
  69. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -0
  70. package/packages/dd-trace/src/llmobs/plugins/vertexai.js +2 -1
  71. package/packages/dd-trace/src/llmobs/writers/spans/base.js +3 -3
  72. package/packages/dd-trace/src/log/index.js +2 -0
  73. package/packages/dd-trace/src/log/writer.js +19 -2
  74. package/packages/dd-trace/src/opentelemetry/span.js +4 -4
  75. package/packages/dd-trace/src/opentracing/propagation/text_map.js +17 -3
  76. package/packages/dd-trace/src/opentracing/span.js +10 -0
  77. package/packages/dd-trace/src/plugin_manager.js +2 -0
  78. package/packages/dd-trace/src/plugins/util/test.js +11 -0
  79. package/packages/dd-trace/src/profiler.js +1 -1
  80. package/packages/dd-trace/src/profiling/config.js +6 -0
  81. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -5
  82. package/packages/dd-trace/src/profiling/profiler.js +4 -3
  83. package/packages/dd-trace/src/profiling/profilers/wall.js +12 -8
  84. package/packages/dd-trace/src/proxy.js +5 -1
  85. package/packages/dd-trace/src/tagger.js +38 -26
  86. package/packages/dd-trace/src/util.js +1 -7
@@ -2,7 +2,11 @@ const satisfies = require('semifies')
2
2
 
3
3
  const { addHook, channel, AsyncResource } = require('./helpers/instrument')
4
4
  const shimmer = require('../../datadog-shimmer')
5
- const { parseAnnotations, getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
5
+ const {
6
+ parseAnnotations,
7
+ getTestSuitePath,
8
+ PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE
9
+ } = require('../../dd-trace/src/plugins/util/test')
6
10
  const log = require('../../dd-trace/src/log')
7
11
 
8
12
  const testStartCh = channel('ci:playwright:test:start')
@@ -18,6 +22,9 @@ const testManagementTestsCh = channel('ci:playwright:test-management-tests')
18
22
  const testSuiteStartCh = channel('ci:playwright:test-suite:start')
19
23
  const testSuiteFinishCh = channel('ci:playwright:test-suite:finish')
20
24
 
25
+ const workerReportCh = channel('ci:playwright:worker:report')
26
+ const testPageGotoCh = channel('ci:playwright:test:page-goto')
27
+
21
28
  const testToAr = new WeakMap()
22
29
  const testSuiteToAr = new Map()
23
30
  const testSuiteToTestStatuses = new Map()
@@ -255,21 +262,19 @@ function getTestFullname (test) {
255
262
  return names.join(' ')
256
263
  }
257
264
 
258
- function testBeginHandler (test, browserName) {
265
+ function testBeginHandler (test, browserName, isMainProcess) {
259
266
  const {
260
267
  _requireFile: testSuiteAbsolutePath,
261
- _type,
262
268
  location: {
263
269
  line: testSourceLine
264
- }
270
+ },
271
+ _type
265
272
  } = test
266
273
 
267
274
  if (_type === 'beforeAll' || _type === 'afterAll') {
268
275
  return
269
276
  }
270
277
 
271
- const testName = getTestFullname(test)
272
-
273
278
  const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
274
279
 
275
280
  if (isNewTestSuite) {
@@ -286,24 +291,31 @@ function testBeginHandler (test, browserName) {
286
291
  test.retries = 0
287
292
  }
288
293
 
289
- const testAsyncResource = new AsyncResource('bound-anonymous-fn')
290
- testToAr.set(test, testAsyncResource)
291
- testAsyncResource.runInAsyncScope(() => {
292
- testStartCh.publish({
293
- testName,
294
- testSuiteAbsolutePath,
295
- testSourceLine,
296
- browserName,
297
- isDisabled: test._ddIsDisabled
294
+ // this handles tests that do not go through the worker process (because they're skipped)
295
+ if (isMainProcess) {
296
+ const testAsyncResource = new AsyncResource('bound-anonymous-fn')
297
+ testToAr.set(test, testAsyncResource)
298
+ const testName = getTestFullname(test)
299
+
300
+ testAsyncResource.runInAsyncScope(() => {
301
+ testStartCh.publish({
302
+ testName,
303
+ testSuiteAbsolutePath,
304
+ testSourceLine,
305
+ browserName,
306
+ isDisabled: test._ddIsDisabled
307
+ })
298
308
  })
299
- })
309
+ }
300
310
  }
301
- function testEndHandler (test, annotations, testStatus, error, isTimeout) {
311
+
312
+ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMainProcess) {
313
+ const { _requireFile: testSuiteAbsolutePath, results, _type } = test
314
+
302
315
  let annotationTags
303
316
  if (annotations.length) {
304
317
  annotationTags = parseAnnotations(annotations)
305
318
  }
306
- const { _requireFile: testSuiteAbsolutePath, results, _type } = test
307
319
 
308
320
  if (_type === 'beforeAll' || _type === 'afterAll') {
309
321
  const hookError = formatTestHookError(error, _type, isTimeout)
@@ -324,35 +336,40 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout) {
324
336
  testStatuses.push(testStatus)
325
337
  }
326
338
 
327
- let hasFailedAllRetries = false
328
- let hasPassedAttemptToFixRetries = false
329
-
330
339
  if (testStatuses.length === testManagementAttemptToFixRetries + 1) {
331
340
  if (testStatuses.every(status => status === 'fail')) {
332
- hasFailedAllRetries = true
341
+ test._ddHasFailedAllRetries = true
333
342
  } else if (testStatuses.every(status => status === 'pass')) {
334
- hasPassedAttemptToFixRetries = true
343
+ test._ddHasPassedAttemptToFixRetries = true
335
344
  }
336
345
  }
337
346
 
338
- const testResult = results[results.length - 1]
339
- const testAsyncResource = testToAr.get(test)
340
- testAsyncResource.runInAsyncScope(() => {
341
- testFinishCh.publish({
342
- testStatus,
343
- steps: testResult?.steps || [],
344
- isRetry: testResult?.retry > 0,
345
- error,
346
- extraTags: annotationTags,
347
- isNew: test._ddIsNew,
348
- isAttemptToFix: test._ddIsAttemptToFix,
349
- isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
350
- isQuarantined: test._ddIsQuarantined,
351
- isEfdRetry: test._ddIsEfdRetry,
352
- hasFailedAllRetries,
353
- hasPassedAttemptToFixRetries
347
+ // this handles tests that do not go through the worker process (because they're skipped)
348
+ if (isMainProcess) {
349
+ const testResult = results[results.length - 1]
350
+ const testAsyncResource = testToAr.get(test)
351
+ const isAtrRetry = testResult?.retry > 0 &&
352
+ isFlakyTestRetriesEnabled &&
353
+ !test._ddIsAttemptToFix &&
354
+ !test._ddIsEfdRetry
355
+ testAsyncResource.runInAsyncScope(() => {
356
+ testFinishCh.publish({
357
+ testStatus,
358
+ steps: testResult?.steps || [],
359
+ isRetry: testResult?.retry > 0,
360
+ error,
361
+ extraTags: annotationTags,
362
+ isNew: test._ddIsNew,
363
+ isAttemptToFix: test._ddIsAttemptToFix,
364
+ isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
365
+ isQuarantined: test._ddIsQuarantined,
366
+ isEfdRetry: test._ddIsEfdRetry,
367
+ hasFailedAllRetries: test._ddHasFailedAllRetries,
368
+ hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
369
+ isAtrRetry
370
+ })
354
371
  })
355
- })
372
+ }
356
373
 
357
374
  if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
358
375
  testSuiteToTestStatuses.get(testSuiteAbsolutePath).push(testStatus)
@@ -416,7 +433,7 @@ function dispatcherHook (dispatcherExport) {
416
433
  const { test } = dispatcher._testById.get(params.testId)
417
434
  const projects = getProjectsFromDispatcher(dispatcher)
418
435
  const browser = getBrowserNameFromProjects(projects, test)
419
- testBeginHandler(test, browser)
436
+ testBeginHandler(test, browser, true)
420
437
  } else if (method === 'testEnd') {
421
438
  const { test } = dispatcher._testById.get(params.testId)
422
439
 
@@ -424,7 +441,14 @@ function dispatcherHook (dispatcherExport) {
424
441
  const testResult = results[results.length - 1]
425
442
 
426
443
  const isTimeout = testResult.status === 'timedOut'
427
- testEndHandler(test, params.annotations, STATUS_TO_TEST_STATUS[testResult.status], testResult.error, isTimeout)
444
+ testEndHandler(
445
+ test,
446
+ params.annotations,
447
+ STATUS_TO_TEST_STATUS[testResult.status],
448
+ testResult.error,
449
+ isTimeout,
450
+ true
451
+ )
428
452
  }
429
453
  })
430
454
 
@@ -443,13 +467,34 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
443
467
  const test = getTestByTestId(dispatcher, testId)
444
468
  const projects = getProjectsFromDispatcher(dispatcher)
445
469
  const browser = getBrowserNameFromProjects(projects, test)
446
- testBeginHandler(test, browser)
470
+ testBeginHandler(test, browser, false)
447
471
  })
448
472
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
449
473
  const test = getTestByTestId(dispatcher, testId)
450
474
 
451
475
  const isTimeout = status === 'timedOut'
452
- testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout)
476
+ testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout, false)
477
+ const testResult = test.results[test.results.length - 1]
478
+ const isAtrRetry = testResult?.retry > 0 &&
479
+ isFlakyTestRetriesEnabled &&
480
+ !test._ddIsAttemptToFix &&
481
+ !test._ddIsEfdRetry
482
+ // We want to send the ddProperties to the worker
483
+ worker.process.send({
484
+ type: 'ddProperties',
485
+ testId: test.id,
486
+ properties: {
487
+ _ddIsDisabled: test._ddIsDisabled,
488
+ _ddIsQuarantined: test._ddIsQuarantined,
489
+ _ddIsAttemptToFix: test._ddIsAttemptToFix,
490
+ _ddIsAttemptToFixRetry: test._ddIsAttemptToFixRetry,
491
+ _ddIsNew: test._ddIsNew,
492
+ _ddIsEfdRetry: test._ddIsEfdRetry,
493
+ _ddHasFailedAllRetries: test._ddHasFailedAllRetries,
494
+ _ddHasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
495
+ _ddIsAtrRetry: isAtrRetry
496
+ }
497
+ })
453
498
  })
454
499
 
455
500
  return worker
@@ -538,8 +583,8 @@ function runnerHook (runnerExport, playwrightVersion) {
538
583
  // because they were skipped
539
584
  tests.forEach(test => {
540
585
  const browser = getBrowserNameFromProjects(projects, test)
541
- testBeginHandler(test, browser)
542
- testEndHandler(test, [], 'skip')
586
+ testBeginHandler(test, browser, true)
587
+ testEndHandler(test, [], 'skip', null, false, true)
543
588
  })
544
589
  })
545
590
 
@@ -720,3 +765,245 @@ addHook({
720
765
 
721
766
  return loadUtilsPackage
722
767
  })
768
+
769
+ // main process hook
770
+ addHook({
771
+ name: 'playwright',
772
+ file: 'lib/runner/processHost.js',
773
+ versions: ['>=1.38.0']
774
+ }, (processHostPackage) => {
775
+ shimmer.wrap(processHostPackage.ProcessHost.prototype, 'startRunner', startRunner => async function () {
776
+ this._extraEnv = {
777
+ ...this._extraEnv,
778
+ // Used to detect that we're in a playwright worker
779
+ DD_PLAYWRIGHT_WORKER: '1'
780
+ }
781
+
782
+ const res = await startRunner.apply(this, arguments)
783
+
784
+ // We add a new listener to `this.process`, which is represents the worker
785
+ this.process.on('message', (message) => {
786
+ // These messages are [code, payload]. The payload is test data
787
+ if (Array.isArray(message) && message[0] === PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE) {
788
+ workerReportCh.publish(message[1])
789
+ }
790
+ })
791
+
792
+ return res
793
+ })
794
+
795
+ return processHostPackage
796
+ })
797
+
798
+ addHook({
799
+ name: 'playwright-core',
800
+ file: 'lib/client/page.js',
801
+ versions: ['>=1.38.0']
802
+ }, (pagePackage) => {
803
+ shimmer.wrap(pagePackage.Page.prototype, 'goto', goto => async function (url, options) {
804
+ const response = await goto.apply(this, arguments)
805
+
806
+ const page = this
807
+
808
+ const isRumActive = await page.evaluate(() => {
809
+ if (window.DD_RUM && window.DD_RUM.getInternalContext) {
810
+ return !!window.DD_RUM.getInternalContext()
811
+ } else {
812
+ return false
813
+ }
814
+ })
815
+
816
+ if (isRumActive) {
817
+ testPageGotoCh.publish({
818
+ isRumActive,
819
+ page
820
+ })
821
+ }
822
+
823
+ return response
824
+ })
825
+
826
+ return pagePackage
827
+ })
828
+
829
+ // Only in worker
830
+ addHook({
831
+ name: 'playwright',
832
+ file: 'lib/worker/workerMain.js',
833
+ versions: ['>=1.38.0']
834
+ }, (workerPackage) => {
835
+ // we assume there's only a test running at a time
836
+ let steps = []
837
+ const stepInfoByStepId = {}
838
+
839
+ shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
840
+ steps = []
841
+
842
+ const {
843
+ _requireFile: testSuiteAbsolutePath,
844
+ location: {
845
+ line: testSourceLine
846
+ }
847
+ } = test
848
+ let res
849
+
850
+ let testInfo
851
+ const testName = getTestFullname(test)
852
+ const browserName = this._project.project.name
853
+
854
+ // If test events are created in the worker process I need to stop creating it in the main process
855
+ // Probably yet another test worker exporter is needed in addition to the ones for mocha, jest and cucumber
856
+ // it's probably hard to tell that's a playwright worker though, as I don't think there is a specific env variable
857
+ const testAsyncResource = new AsyncResource('bound-anonymous-fn')
858
+ // TODO - In the future we may need to implement a mechanism to send test properties
859
+ // to the worker process before _runTest is called
860
+ testAsyncResource.runInAsyncScope(() => {
861
+ testStartCh.publish({
862
+ testName,
863
+ testSuiteAbsolutePath,
864
+ testSourceLine,
865
+ browserName
866
+ })
867
+
868
+ let existAfterEachHook = false
869
+
870
+ // We try to find an existing afterEach hook with _ddHook to avoid adding a new one
871
+ for (const hook of test.parent._hooks) {
872
+ if (hook.type === 'afterEach' && hook._ddHook) {
873
+ existAfterEachHook = true
874
+ break
875
+ }
876
+ }
877
+
878
+ // In cases where there is no afterEach hook with _ddHook, we need to add one
879
+ if (!existAfterEachHook) {
880
+ test.parent._hooks.push({
881
+ type: 'afterEach',
882
+ fn: async function ({ page }) {
883
+ try {
884
+ if (page) {
885
+ const isRumActive = await page.evaluate(() => {
886
+ if (window.DD_RUM && window.DD_RUM.stopSession) {
887
+ window.DD_RUM.stopSession()
888
+ return true
889
+ } else {
890
+ return false
891
+ }
892
+ })
893
+
894
+ if (isRumActive) {
895
+ const url = page.url()
896
+ if (url) {
897
+ const domain = new URL(url).hostname
898
+ await page.context().addCookies([{
899
+ name: 'datadog-ci-visibility-test-execution-id',
900
+ value: '',
901
+ domain,
902
+ expires: 0,
903
+ path: '/'
904
+ }])
905
+ }
906
+ }
907
+ }
908
+ } catch (e) {
909
+ // ignore errors
910
+ }
911
+ },
912
+ title: 'afterEach hook',
913
+ _ddHook: true
914
+ })
915
+ }
916
+
917
+ res = _runTest.apply(this, arguments)
918
+
919
+ testInfo = this._currentTest
920
+ })
921
+ await res
922
+
923
+ const { status, error, annotations, retry, testId } = testInfo
924
+
925
+ // testInfo.errors could be better than "error",
926
+ // which will only include timeout error (even though the test failed because of a different error)
927
+
928
+ let annotationTags
929
+ if (annotations.length) {
930
+ annotationTags = parseAnnotations(annotations)
931
+ }
932
+
933
+ let onDone
934
+
935
+ const flushPromise = new Promise(resolve => {
936
+ onDone = resolve
937
+ })
938
+
939
+ // Wait for ddProperties to be received and processed
940
+ // Create a promise that will be resolved when the properties are received
941
+ const ddPropertiesPromise = new Promise(resolve => {
942
+ const messageHandler = ({ type, testId, properties }) => {
943
+ if (type === 'ddProperties' && testId === test.id) {
944
+ // Apply the properties to the test object
945
+ if (properties) {
946
+ Object.assign(test, properties)
947
+ }
948
+ process.removeListener('message', messageHandler)
949
+ resolve()
950
+ }
951
+ }
952
+
953
+ // Add the listener
954
+ process.on('message', messageHandler)
955
+ })
956
+
957
+ // Wait for the properties to be received
958
+ await ddPropertiesPromise
959
+
960
+ testAsyncResource.runInAsyncScope(() => {
961
+ testFinishCh.publish({
962
+ testStatus: STATUS_TO_TEST_STATUS[status],
963
+ steps: steps.filter(step => step.testId === testId),
964
+ error,
965
+ extraTags: annotationTags,
966
+ isNew: test._ddIsNew,
967
+ isRetry: retry > 0,
968
+ isEfdRetry: test._ddIsEfdRetry,
969
+ isAttemptToFix: test._ddIsAttemptToFix,
970
+ isDisabled: test._ddIsDisabled,
971
+ isQuarantined: test._ddIsQuarantined,
972
+ isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
973
+ hasFailedAllRetries: test._ddHasFailedAllRetries,
974
+ hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
975
+ isAtrRetry: test._ddIsAtrRetry,
976
+ onDone
977
+ })
978
+ })
979
+
980
+ await flushPromise
981
+
982
+ return res
983
+ })
984
+
985
+ // We reproduce what happens in `Dispatcher#_onStepBegin` and `Dispatcher#_onStepEnd`,
986
+ // since `startTime` and `duration` are not available directly in the worker process
987
+ shimmer.wrap(workerPackage.WorkerMain.prototype, 'dispatchEvent', dispatchEvent => function (event, payload) {
988
+ if (event === 'stepBegin') {
989
+ stepInfoByStepId[payload.stepId] = {
990
+ startTime: payload.wallTime,
991
+ title: payload.title,
992
+ testId: payload.testId
993
+ }
994
+ } else if (event === 'stepEnd') {
995
+ const stepInfo = stepInfoByStepId[payload.stepId]
996
+ delete stepInfoByStepId[payload.stepId]
997
+ steps.push({
998
+ testId: stepInfo.testId,
999
+ startTime: new Date(stepInfo.startTime),
1000
+ title: stepInfo.title,
1001
+ duration: payload.wallTime - stepInfo.startTime,
1002
+ error: payload.error
1003
+ })
1004
+ }
1005
+ return dispatchEvent.apply(this, arguments)
1006
+ })
1007
+
1008
+ return workerPackage
1009
+ })
@@ -19,7 +19,7 @@ function createWrapRouterMethod (name) {
19
19
  function wrapLayerHandle (layer, original) {
20
20
  original._name = original._name || layer.name
21
21
 
22
- const handle = shimmer.wrapFunction(original, original => function () {
22
+ return shimmer.wrapFunction(original, original => function () {
23
23
  if (!enterChannel.hasSubscribers) return original.apply(this, arguments)
24
24
 
25
25
  const matchers = layerMatchers.get(layer)
@@ -59,12 +59,6 @@ function createWrapRouterMethod (name) {
59
59
  exitChannel.publish({ req })
60
60
  }
61
61
  })
62
-
63
- // This is a workaround for the `loopback` library so that it can find the correct express layer
64
- // that contains the real handle function
65
- handle._datadog_orig = original
66
-
67
- return handle
68
62
  }
69
63
 
70
64
  function wrapStack (stack, offset, matchers) {
@@ -82,7 +82,8 @@ function getProvidedContext () {
82
82
  isKnownTestsEnabled: false,
83
83
  isTestManagementTestsEnabled: false,
84
84
  testManagementAttemptToFixRetries: 0,
85
- testManagementTests: {}
85
+ testManagementTests: {},
86
+ isFlakyTestRetriesEnabled: false
86
87
  }
87
88
  }
88
89
  }
@@ -466,7 +467,8 @@ addHook({
466
467
  isEarlyFlakeDetectionEnabled,
467
468
  isDiEnabled,
468
469
  isTestManagementTestsEnabled,
469
- testManagementTests
470
+ testManagementTests,
471
+ isFlakyTestRetriesEnabled
470
472
  } = getProvidedContext()
471
473
 
472
474
  if (isKnownTestsEnabled) {
@@ -566,6 +568,11 @@ addHook({
566
568
  }
567
569
  }
568
570
 
571
+ const isRetryReasonAtr = numAttempt > 0 &&
572
+ isFlakyTestRetriesEnabled &&
573
+ !isRetryReasonAttemptToFix &&
574
+ !isRetryReasonEfd
575
+
569
576
  const asyncResource = new AsyncResource('bound-anonymous-fn')
570
577
  taskToAsync.set(task, asyncResource)
571
578
 
@@ -580,7 +587,8 @@ addHook({
580
587
  mightHitProbe: isDiEnabled && numAttempt > 0,
581
588
  isAttemptToFix: attemptToFixTasks.has(task),
582
589
  isDisabled: disabledTasks.has(task),
583
- isQuarantined
590
+ isQuarantined,
591
+ isRetryReasonAtr
584
592
  })
585
593
  })
586
594
  return onBeforeTryTask.apply(this, arguments)
@@ -33,7 +33,8 @@ const {
33
33
  TEST_MANAGEMENT_IS_DISABLED,
34
34
  TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
35
35
  TEST_HAS_FAILED_ALL_RETRIES,
36
- TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED
36
+ TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
37
+ TEST_RETRY_REASON_TYPES
37
38
  } = require('../../dd-trace/src/plugins/util/test')
38
39
  const { RESOURCE_NAME } = require('../../../ext/tags')
39
40
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -252,11 +253,16 @@ class CucumberPlugin extends CiPlugin {
252
253
  }
253
254
  })
254
255
 
255
- this.addSub('ci:cucumber:test:retry', ({ isFirstAttempt, error }) => {
256
+ this.addSub('ci:cucumber:test:retry', ({ isFirstAttempt, error, isAtrRetry }) => {
256
257
  const store = storage('legacy').getStore()
257
258
  const span = store.span
258
259
  if (!isFirstAttempt) {
259
260
  span.setTag(TEST_IS_RETRY, 'true')
261
+ if (isAtrRetry) {
262
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
263
+ } else {
264
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
265
+ }
260
266
  }
261
267
  span.setTag('error', error)
262
268
  if (isFirstAttempt && this.di && error && this.libraryConfig?.isDiEnabled) {
@@ -347,7 +353,7 @@ class CucumberPlugin extends CiPlugin {
347
353
  span.setTag(TEST_IS_NEW, 'true')
348
354
  if (isEfdRetry) {
349
355
  span.setTag(TEST_IS_RETRY, 'true')
350
- span.setTag(TEST_RETRY_REASON, 'efd')
356
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
351
357
  }
352
358
  }
353
359
 
@@ -363,6 +369,7 @@ class CucumberPlugin extends CiPlugin {
363
369
 
364
370
  if (isFlakyRetry > 0) {
365
371
  span.setTag(TEST_IS_RETRY, 'true')
372
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
366
373
  }
367
374
 
368
375
  if (hasFailedAllRetries) {
@@ -375,7 +382,7 @@ class CucumberPlugin extends CiPlugin {
375
382
 
376
383
  if (isAttemptToFixRetry) {
377
384
  span.setTag(TEST_IS_RETRY, 'true')
378
- span.setTag(TEST_RETRY_REASON, 'attempt_to_fix')
385
+ span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
379
386
  if (hasPassedAllRetries) {
380
387
  span.setTag(TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED, 'true')
381
388
  }
@@ -40,7 +40,8 @@ const {
40
40
  TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX,
41
41
  TEST_MANAGEMENT_ATTEMPT_TO_FIX_PASSED,
42
42
  TEST_HAS_FAILED_ALL_RETRIES,
43
- getLibraryCapabilitiesTags
43
+ getLibraryCapabilitiesTags,
44
+ TEST_RETRY_REASON_TYPES
44
45
  } = require('../../dd-trace/src/plugins/util/test')
45
46
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
46
47
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -229,6 +230,7 @@ class CypressPlugin {
229
230
  this.isTestsSkipped = false
230
231
  this.isSuitesSkippingEnabled = false
231
232
  this.isCodeCoverageEnabled = false
233
+ this.isFlakyTestRetriesEnabled = false
232
234
  this.isEarlyFlakeDetectionEnabled = false
233
235
  this.isKnownTestsEnabled = false
234
236
  this.earlyFlakeDetectionNumRetries = 0
@@ -278,6 +280,7 @@ class CypressPlugin {
278
280
  this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
279
281
  this.isKnownTestsEnabled = isKnownTestsEnabled
280
282
  if (isFlakyTestRetriesEnabled) {
283
+ this.isFlakyTestRetriesEnabled = true
281
284
  this.cypressConfig.retries.runMode = flakyTestRetriesCount
282
285
  }
283
286
  this.isTestManagementTestsEnabled = isTestManagementEnabled
@@ -654,10 +657,18 @@ class CypressPlugin {
654
657
  let cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
655
658
  if (cypressTest.attempts && cypressTest.attempts[attemptIndex]) {
656
659
  cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
660
+ const isAtrRetry = attemptIndex > 0 &&
661
+ this.isFlakyTestRetriesEnabled &&
662
+ !finishedTest.isAttemptToFix &&
663
+ !finishedTest.isEfdRetry
657
664
  if (attemptIndex > 0) {
658
665
  finishedTest.testSpan.setTag(TEST_IS_RETRY, 'true')
659
666
  if (finishedTest.isEfdRetry) {
660
- finishedTest.testSpan.setTag(TEST_RETRY_REASON, 'efd')
667
+ finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
668
+ } else if (isAtrRetry) {
669
+ finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
670
+ } else {
671
+ finishedTest.testSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.ext)
661
672
  }
662
673
  }
663
674
  }
@@ -816,14 +827,14 @@ class CypressPlugin {
816
827
  this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
817
828
  if (isEfdRetry) {
818
829
  this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
819
- this.activeTestSpan.setTag(TEST_RETRY_REASON, 'efd')
830
+ this.activeTestSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
820
831
  }
821
832
  }
822
833
  if (isAttemptToFix) {
823
834
  this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
824
835
  if (testStatuses.length > 1) {
825
836
  this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
826
- this.activeTestSpan.setTag(TEST_RETRY_REASON, 'attempt_to_fix')
837
+ this.activeTestSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atf)
827
838
  }
828
839
  const isLastAttempt = testStatuses.length === this.testManagementAttemptToFixRetries + 1
829
840
  if (isLastAttempt) {
@@ -840,7 +851,8 @@ class CypressPlugin {
840
851
  testStatus,
841
852
  finishTime: this.activeTestSpan._getTime(), // we store the finish time here
842
853
  testSpan: this.activeTestSpan,
843
- isEfdRetry
854
+ isEfdRetry,
855
+ isAttemptToFix
844
856
  }
845
857
  if (this.finishedTestsByFile[testSuite]) {
846
858
  this.finishedTestsByFile[testSuite].push(finishedTest)