dd-trace 5.73.0 → 5.75.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 (37) hide show
  1. package/LICENSE-3rdparty.csv +2 -0
  2. package/index.d.ts +39 -7
  3. package/loader-hook.mjs +52 -1
  4. package/package.json +8 -16
  5. package/packages/datadog-core/src/utils/src/set.js +5 -1
  6. package/packages/datadog-esbuild/index.js +105 -36
  7. package/packages/datadog-esbuild/src/utils.js +198 -0
  8. package/packages/datadog-instrumentations/src/cookie-parser.js +0 -2
  9. package/packages/datadog-instrumentations/src/cucumber.js +2 -2
  10. package/packages/datadog-instrumentations/src/express.js +82 -0
  11. package/packages/datadog-instrumentations/src/helpers/router-helper.js +238 -0
  12. package/packages/datadog-instrumentations/src/jest.js +2 -1
  13. package/packages/datadog-instrumentations/src/mariadb.js +9 -7
  14. package/packages/datadog-instrumentations/src/playwright.js +226 -93
  15. package/packages/datadog-instrumentations/src/router.js +63 -6
  16. package/packages/datadog-instrumentations/src/vitest.js +44 -12
  17. package/packages/datadog-instrumentations/src/ws.js +3 -3
  18. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -1
  19. package/packages/datadog-plugin-express/src/code_origin.js +2 -0
  20. package/packages/datadog-plugin-playwright/src/index.js +74 -31
  21. package/packages/datadog-plugin-ws/src/close.js +1 -1
  22. package/packages/datadog-shimmer/src/shimmer.js +2 -0
  23. package/packages/dd-trace/src/aiguard/sdk.js +25 -3
  24. package/packages/dd-trace/src/aiguard/tags.js +4 -1
  25. package/packages/dd-trace/src/config-helper.js +4 -1
  26. package/packages/dd-trace/src/config.js +599 -592
  27. package/packages/dd-trace/src/config_defaults.js +14 -12
  28. package/packages/dd-trace/src/plugins/util/ci.js +3 -2
  29. package/packages/dd-trace/src/plugins/util/stacktrace.js +16 -1
  30. package/packages/dd-trace/src/proxy.js +1 -1
  31. package/packages/dd-trace/src/supported-configurations.json +1 -0
  32. package/packages/dd-trace/src/telemetry/endpoints.js +27 -1
  33. package/packages/dd-trace/src/telemetry/index.js +16 -13
  34. package/packages/dd-trace/src/telemetry/logs/log-collector.js +5 -3
  35. package/register.js +1 -11
  36. package/scripts/preinstall.js +3 -1
  37. package/version.js +2 -1
@@ -7,13 +7,15 @@ const shimmer = require('../../datadog-shimmer')
7
7
  const {
8
8
  parseAnnotations,
9
9
  getTestSuitePath,
10
- PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE
10
+ PLAYWRIGHT_WORKER_TRACE_PAYLOAD_CODE,
11
+ getIsFaultyEarlyFlakeDetection
11
12
  } = require('../../dd-trace/src/plugins/util/test')
12
13
  const log = require('../../dd-trace/src/log')
13
14
  const { DD_MAJOR } = require('../../../version')
14
15
 
15
16
  const testStartCh = channel('ci:playwright:test:start')
16
17
  const testFinishCh = channel('ci:playwright:test:finish')
18
+ const testSkipCh = channel('ci:playwright:test:skip')
17
19
 
18
20
  const testSessionStartCh = channel('ci:playwright:session:start')
19
21
  const testSessionFinishCh = channel('ci:playwright:session:finish')
@@ -36,6 +38,8 @@ const testSuiteToTestStatuses = new Map()
36
38
  const testSuiteToErrors = new Map()
37
39
  const testsToTestStatuses = new Map()
38
40
 
41
+ const RUM_FLUSH_WAIT_TIME = 1000
42
+
39
43
  let applyRepeatEachIndex = null
40
44
 
41
45
  let startedSuites = []
@@ -52,6 +56,7 @@ let isKnownTestsEnabled = false
52
56
  let isEarlyFlakeDetectionEnabled = false
53
57
  let earlyFlakeDetectionNumRetries = 0
54
58
  let isEarlyFlakeDetectionFaulty = false
59
+ let earlyFlakeDetectionFaultyThreshold = 0
55
60
  let isFlakyTestRetriesEnabled = false
56
61
  let flakyTestRetriesCount = 0
57
62
  let knownTests = {}
@@ -111,8 +116,10 @@ function deepCloneSuite (suite, filterTest, tags = []) {
111
116
  if (filterTest(entry)) {
112
117
  const copiedTest = entry._clone()
113
118
  tags.forEach(tag => {
114
- if (tag) {
115
- copiedTest[tag] = true
119
+ const resolvedTag = typeof tag === 'function' ? tag(entry) : tag
120
+
121
+ if (resolvedTag) {
122
+ copiedTest[resolvedTag] = true
116
123
  }
117
124
  })
118
125
  copy._addTest(copiedTest)
@@ -281,7 +288,12 @@ function getTestFullname (test) {
281
288
  return names.join(' ')
282
289
  }
283
290
 
284
- function testBeginHandler (test, browserName, isMainProcess) {
291
+ function shouldFinishTestSuite (testSuiteAbsolutePath) {
292
+ const remainingTests = remainingTestsByFile[testSuiteAbsolutePath]
293
+ return !remainingTests.length || remainingTests.every(test => test.expectedStatus === 'skipped')
294
+ }
295
+
296
+ function testBeginHandler (test, browserName, shouldCreateTestSpan) {
285
297
  const {
286
298
  _requireFile: testSuiteAbsolutePath,
287
299
  location: {
@@ -293,6 +305,10 @@ function testBeginHandler (test, browserName, isMainProcess) {
293
305
  if (_type === 'beforeAll' || _type === 'afterAll') {
294
306
  return
295
307
  }
308
+ // this means that a skipped test is being handled
309
+ if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
310
+ return
311
+ }
296
312
 
297
313
  const isNewTestSuite = !startedSuites.includes(testSuiteAbsolutePath)
298
314
 
@@ -309,7 +325,7 @@ function testBeginHandler (test, browserName, isMainProcess) {
309
325
  }
310
326
 
311
327
  // this handles tests that do not go through the worker process (because they're skipped)
312
- if (isMainProcess) {
328
+ if (shouldCreateTestSpan) {
313
329
  const testName = getTestFullname(test)
314
330
  const testCtx = {
315
331
  testName,
@@ -324,8 +340,20 @@ function testBeginHandler (test, browserName, isMainProcess) {
324
340
  }
325
341
  }
326
342
 
327
- function testEndHandler (test, annotations, testStatus, error, isTimeout, isMainProcess) {
328
- const { _requireFile: testSuiteAbsolutePath, results, _type } = test
343
+ function testEndHandler ({
344
+ test,
345
+ annotations,
346
+ testStatus,
347
+ error,
348
+ isTimeout,
349
+ shouldCreateTestSpan,
350
+ projects
351
+ }) {
352
+ const {
353
+ _requireFile: testSuiteAbsolutePath,
354
+ results,
355
+ _type,
356
+ } = test
329
357
 
330
358
  let annotationTags
331
359
  if (annotations.length) {
@@ -364,31 +392,34 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
364
392
  }
365
393
 
366
394
  // this handles tests that do not go through the worker process (because they're skipped)
367
- if (isMainProcess) {
395
+ if (shouldCreateTestSpan) {
368
396
  const testResult = results.at(-1)
369
397
  const testCtx = testToCtx.get(test)
370
398
  const isAtrRetry = testResult?.retry > 0 &&
371
399
  isFlakyTestRetriesEnabled &&
372
400
  !test._ddIsAttemptToFix &&
373
401
  !test._ddIsEfdRetry
374
- testFinishCh.publish({
375
- testStatus,
376
- steps: testResult?.steps || [],
377
- isRetry: testResult?.retry > 0,
378
- error,
379
- extraTags: annotationTags,
380
- isNew: test._ddIsNew,
381
- isAttemptToFix: test._ddIsAttemptToFix,
382
- isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
383
- isQuarantined: test._ddIsQuarantined,
384
- isEfdRetry: test._ddIsEfdRetry,
385
- hasFailedAllRetries: test._ddHasFailedAllRetries,
386
- hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
387
- hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
388
- isAtrRetry,
389
- isModified: test._ddIsModified,
390
- ...testCtx.currentStore
391
- })
402
+ // if there is no testCtx, the skipped test will be created later
403
+ if (testCtx) {
404
+ testFinishCh.publish({
405
+ testStatus,
406
+ steps: testResult?.steps || [],
407
+ isRetry: testResult?.retry > 0,
408
+ error,
409
+ extraTags: annotationTags,
410
+ isNew: test._ddIsNew,
411
+ isAttemptToFix: test._ddIsAttemptToFix,
412
+ isAttemptToFixRetry: test._ddIsAttemptToFixRetry,
413
+ isQuarantined: test._ddIsQuarantined,
414
+ isEfdRetry: test._ddIsEfdRetry,
415
+ hasFailedAllRetries: test._ddHasFailedAllRetries,
416
+ hasPassedAttemptToFixRetries: test._ddHasPassedAttemptToFixRetries,
417
+ hasFailedAttemptToFixRetries: test._ddHasFailedAttemptToFixRetries,
418
+ isAtrRetry,
419
+ isModified: test._ddIsModified,
420
+ ...testCtx.currentStore
421
+ })
422
+ }
392
423
  }
393
424
 
394
425
  if (testSuiteToTestStatuses.has(testSuiteAbsolutePath)) {
@@ -406,8 +437,25 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
406
437
  .filter(currentTest => currentTest !== test)
407
438
  }
408
439
 
409
- // Last test, we finish the suite
410
- if (!remainingTestsByFile[testSuiteAbsolutePath].length) {
440
+ if (shouldFinishTestSuite(testSuiteAbsolutePath)) {
441
+ const skippedTests = remainingTestsByFile[testSuiteAbsolutePath]
442
+ .filter(test => test.expectedStatus === 'skipped')
443
+
444
+ for (const test of skippedTests) {
445
+ const browserName = getBrowserNameFromProjects(projects, test)
446
+ testSkipCh.publish({
447
+ testName: getTestFullname(test),
448
+ testSuiteAbsolutePath,
449
+ testSourceLine: test.location.line,
450
+ browserName,
451
+ isNew: test._ddIsNew,
452
+ isDisabled: test._ddIsDisabled,
453
+ isModified: test._ddIsModified,
454
+ isQuarantined: test._ddIsQuarantined
455
+ })
456
+ }
457
+ remainingTestsByFile[testSuiteAbsolutePath] = []
458
+
411
459
  const testStatuses = testSuiteToTestStatuses.get(testSuiteAbsolutePath)
412
460
  let testSuiteStatus = 'pass'
413
461
  if (testStatuses.includes('fail')) {
@@ -446,10 +494,13 @@ function dispatcherHook (dispatcherExport) {
446
494
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
447
495
  const dispatcher = this
448
496
  const worker = createWorker.apply(this, arguments)
497
+ const projects = getProjectsFromDispatcher(dispatcher)
498
+
499
+ // for older versions of playwright, `shouldCreateTestSpan` should always be true,
500
+ // since the `_runTest` function wrapper is not available for older versions
449
501
  worker.process.on('message', ({ method, params }) => {
450
502
  if (method === 'testBegin') {
451
503
  const { test } = dispatcher._testById.get(params.testId)
452
- const projects = getProjectsFromDispatcher(dispatcher)
453
504
  const browser = getBrowserNameFromProjects(projects, test)
454
505
  testBeginHandler(test, browser, true)
455
506
  } else if (method === 'testEnd') {
@@ -460,12 +511,15 @@ function dispatcherHook (dispatcherExport) {
460
511
 
461
512
  const isTimeout = testResult.status === 'timedOut'
462
513
  testEndHandler(
463
- test,
464
- params.annotations,
465
- STATUS_TO_TEST_STATUS[testResult.status],
466
- testResult.error,
467
- isTimeout,
468
- true
514
+ {
515
+ test,
516
+ annotations: params.annotations,
517
+ testStatus: STATUS_TO_TEST_STATUS[testResult.status],
518
+ error: testResult.error,
519
+ isTimeout,
520
+ shouldCreateTestSpan: true,
521
+ projects
522
+ }
469
523
  )
470
524
  }
471
525
  })
@@ -480,18 +534,30 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
480
534
  shimmer.wrap(dispatcherExport.Dispatcher.prototype, '_createWorker', createWorker => function () {
481
535
  const dispatcher = this
482
536
  const worker = createWorker.apply(this, arguments)
537
+ const projects = getProjectsFromDispatcher(dispatcher)
483
538
 
484
539
  worker.on('testBegin', ({ testId }) => {
485
540
  const test = getTestByTestId(dispatcher, testId)
486
- const projects = getProjectsFromDispatcher(dispatcher)
487
541
  const browser = getBrowserNameFromProjects(projects, test)
488
- testBeginHandler(test, browser, false)
542
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
543
+ testBeginHandler(test, browser, shouldCreateTestSpan)
489
544
  })
490
545
  worker.on('testEnd', ({ testId, status, errors, annotations }) => {
491
546
  const test = getTestByTestId(dispatcher, testId)
492
547
 
493
548
  const isTimeout = status === 'timedOut'
494
- testEndHandler(test, annotations, STATUS_TO_TEST_STATUS[status], errors && errors[0], isTimeout, false)
549
+ const shouldCreateTestSpan = test.expectedStatus === 'skipped'
550
+ testEndHandler(
551
+ {
552
+ test,
553
+ annotations,
554
+ testStatus: STATUS_TO_TEST_STATUS[status],
555
+ error: errors && errors[0],
556
+ isTimeout,
557
+ shouldCreateTestSpan,
558
+ projects
559
+ }
560
+ )
495
561
  const testResult = test.results.at(-1)
496
562
  const isAtrRetry = testResult?.retry > 0 &&
497
563
  isFlakyTestRetriesEnabled &&
@@ -542,6 +608,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
542
608
  isKnownTestsEnabled = libraryConfig.isKnownTestsEnabled
543
609
  isEarlyFlakeDetectionEnabled = libraryConfig.isEarlyFlakeDetectionEnabled
544
610
  earlyFlakeDetectionNumRetries = libraryConfig.earlyFlakeDetectionNumRetries
611
+ earlyFlakeDetectionFaultyThreshold = libraryConfig.earlyFlakeDetectionFaultyThreshold
545
612
  isFlakyTestRetriesEnabled = libraryConfig.isFlakyTestRetriesEnabled
546
613
  flakyTestRetriesCount = libraryConfig.flakyTestRetriesCount
547
614
  isTestManagementTestsEnabled = libraryConfig.isTestManagementEnabled
@@ -620,6 +687,9 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
620
687
 
621
688
  let runAllTestsReturn = await runAllTests.apply(this, arguments)
622
689
 
690
+ // Tests that have only skipped tests may reach this point
691
+ // Skipped tests may or may not go through `testBegin` or `testEnd`
692
+ // depending on the playwright configuration
623
693
  Object.values(remainingTestsByFile).forEach(tests => {
624
694
  // `tests` should normally be empty, but if it isn't,
625
695
  // there were tests that did not go through `testBegin` or `testEnd`,
@@ -627,7 +697,15 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
627
697
  tests.forEach(test => {
628
698
  const browser = getBrowserNameFromProjects(projects, test)
629
699
  testBeginHandler(test, browser, true)
630
- testEndHandler(test, [], 'skip', null, false, true)
700
+ testEndHandler({
701
+ test,
702
+ annotations: [],
703
+ testStatus: 'skip',
704
+ error: null,
705
+ isTimeout: false,
706
+ shouldCreateTestSpan: true,
707
+ projects
708
+ })
631
709
  })
632
710
  })
633
711
 
@@ -761,6 +839,30 @@ addHook({
761
839
  return suiteUtilsPackage
762
840
  })
763
841
 
842
+ /**
843
+ * We could repeat the logic of `applyRepeatEachIndex` here, but it'd be more risky
844
+ * as playwright could change it at any time.
845
+ *
846
+ * `applyRepeatEachIndex` goes through all the tests in a suite and applies the "repeat" logic
847
+ * for a single repeat index.
848
+ *
849
+ * This means that the clone logic is cumbersome:
850
+ * - we grab the unique file suites that have new tests
851
+ * - we store its project suite
852
+ * - we clone each of these file suites for each repeat index
853
+ * - we execute `applyRepeatEachIndex` for each of these cloned file suites
854
+ * - we add the cloned file suites to the project suite
855
+ */
856
+ function applyRetriesToTests (fileSuitesWithTestsToRetry, filterTest, tagsToApply, numRetries) {
857
+ for (const [fileSuite, projectSuite] of fileSuitesWithTestsToRetry.entries()) {
858
+ for (let repeatEachIndex = 1; repeatEachIndex <= numRetries; repeatEachIndex++) {
859
+ const copyFileSuite = deepCloneSuite(fileSuite, filterTest, tagsToApply)
860
+ applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
861
+ projectSuite._addSuite(copyFileSuite)
862
+ }
863
+ }
864
+ }
865
+
764
866
  addHook({
765
867
  name: 'playwright',
766
868
  file: 'lib/runner/loadUtils.js',
@@ -780,87 +882,112 @@ addHook({
780
882
  const allTests = rootSuite.allTests()
781
883
 
782
884
  if (isTestManagementTestsEnabled) {
885
+ const fileSuitesWithManagedTestsToProjects = new Map()
783
886
  for (const test of allTests) {
784
887
  const testProperties = getTestProperties(test)
888
+ // Disabled tests are skipped and not retried
785
889
  if (testProperties.disabled) {
786
890
  test._ddIsDisabled = true
787
- } else if (testProperties.quarantined) {
891
+ test.expectedStatus = 'skipped'
892
+ continue
893
+ }
894
+ if (testProperties.quarantined) {
788
895
  test._ddIsQuarantined = true
896
+ if (!testProperties.attemptToFix) {
897
+ // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
898
+ const testFqn = getTestFullyQualifiedName(test)
899
+ quarantinedButNotAttemptToFixFqns.add(testFqn)
900
+ }
789
901
  }
790
902
  if (testProperties.attemptToFix) {
791
903
  test._ddIsAttemptToFix = true
792
904
  const fileSuite = getSuiteType(test, 'file')
793
- const projectSuite = getSuiteType(test, 'project')
794
- const isAttemptToFix = test => getTestProperties(test).attemptToFix
795
- for (let repeatEachIndex = 1; repeatEachIndex <= testManagementAttemptToFixRetries; repeatEachIndex++) {
796
- const copyFileSuite = deepCloneSuite(fileSuite, isAttemptToFix, [
797
- testProperties.disabled && '_ddIsDisabled',
798
- testProperties.quarantined && '_ddIsQuarantined',
799
- '_ddIsAttemptToFix',
800
- '_ddIsAttemptToFixRetry'
801
- ])
802
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
803
- projectSuite._addSuite(copyFileSuite)
905
+
906
+ if (!fileSuitesWithManagedTestsToProjects.has(fileSuite)) {
907
+ fileSuitesWithManagedTestsToProjects.set(fileSuite, getSuiteType(test, 'project'))
804
908
  }
805
909
  if (testProperties.disabled || testProperties.quarantined) {
806
910
  quarantinedOrDisabledTestsAttemptToFix.push(test)
807
911
  }
808
- } else if (testProperties.disabled) {
809
- test.expectedStatus = 'skipped'
810
- } else if (testProperties.quarantined) {
811
- // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
812
- const testFqn = getTestFullyQualifiedName(test)
813
- quarantinedButNotAttemptToFixFqns.add(testFqn)
814
912
  }
815
913
  }
914
+ applyRetriesToTests(
915
+ fileSuitesWithManagedTestsToProjects,
916
+ (test) => test._ddIsAttemptToFix,
917
+ [
918
+ (test) => test._ddIsQuarantined && '_ddIsQuarantined',
919
+ '_ddIsAttemptToFix',
920
+ '_ddIsAttemptToFixRetry'
921
+ ],
922
+ testManagementAttemptToFixRetries
923
+ )
816
924
  }
817
925
 
818
926
  if (isImpactedTestsEnabled) {
819
- await Promise.all(allTests.map(async (test) => {
820
- const { isModified } = await getChannelPromise(isModifiedCh, {
927
+ const impactedTests = allTests.filter(test => {
928
+ let isImpacted = false
929
+ isModifiedCh.publish({
821
930
  filePath: test._requireFile,
822
- modifiedFiles
931
+ modifiedFiles,
932
+ onDone: (isModified) => { isImpacted = isModified }
823
933
  })
824
- if (isModified) {
825
- test._ddIsModified = true
826
- }
827
- if (isEarlyFlakeDetectionEnabled && test.expectedStatus !== 'skipped') {
828
- const isNew = isKnownTestsEnabled && isNewTest(test)
829
- const fileSuite = getSuiteType(test, 'file')
830
- const projectSuite = getSuiteType(test, 'project')
831
- // If something change in the file, all tests in the file are impacted
832
- const isModifiedTest = () => isModified
833
- for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
834
- const copyFileSuite = deepCloneSuite(fileSuite, isModifiedTest, [
835
- isNew && '_ddIsNew',
836
- '_ddIsModified',
837
- '_ddIsEfdRetry'
838
- ])
839
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
840
- projectSuite._addSuite(copyFileSuite)
934
+ return isImpacted
935
+ })
936
+
937
+ const fileSuitesWithImpactedTestsToProjects = new Map()
938
+ impactedTests.forEach(impactedTest => {
939
+ impactedTest._ddIsModified = true
940
+ if (isEarlyFlakeDetectionEnabled && impactedTest.expectedStatus !== 'skipped') {
941
+ const fileSuite = getSuiteType(impactedTest, 'file')
942
+ if (!fileSuitesWithImpactedTestsToProjects.has(fileSuite)) {
943
+ fileSuitesWithImpactedTestsToProjects.set(fileSuite, getSuiteType(impactedTest, 'project'))
841
944
  }
842
945
  }
843
- }))
946
+ })
947
+ // If something change in the file, all tests in the file are impacted, hence the () => true filter
948
+ applyRetriesToTests(
949
+ fileSuitesWithImpactedTestsToProjects,
950
+ () => true,
951
+ [
952
+ '_ddIsModified',
953
+ '_ddIsEfdRetry',
954
+ (test) => (isKnownTestsEnabled && isNewTest(test) ? '_ddIsNew' : null)
955
+ ],
956
+ earlyFlakeDetectionNumRetries
957
+ )
844
958
  }
845
959
 
846
960
  if (isKnownTestsEnabled) {
847
961
  const newTests = allTests.filter(isNewTest)
848
962
 
849
- for (const newTest of newTests) {
850
- // No need to filter out attempt to fix tests here because attempt to fix tests are never new
851
- newTest._ddIsNew = true
852
- if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
853
- const fileSuite = getSuiteType(newTest, 'file')
854
- const projectSuite = getSuiteType(newTest, 'project')
855
- for (let repeatEachIndex = 1; repeatEachIndex <= earlyFlakeDetectionNumRetries; repeatEachIndex++) {
856
- const copyFileSuite = deepCloneSuite(fileSuite, isNewTest, [
857
- '_ddIsNew',
858
- '_ddIsEfdRetry'
859
- ])
860
- applyRepeatEachIndex(projectSuite._fullProject, copyFileSuite, repeatEachIndex + 1)
861
- projectSuite._addSuite(copyFileSuite)
963
+ const isFaulty = getIsFaultyEarlyFlakeDetection(
964
+ allTests.map(test => getTestSuitePath(test._requireFile, rootDir)),
965
+ knownTests.playwright,
966
+ earlyFlakeDetectionFaultyThreshold
967
+ )
968
+
969
+ if (isFaulty) {
970
+ isEarlyFlakeDetectionEnabled = false
971
+ isKnownTestsEnabled = false
972
+ isEarlyFlakeDetectionFaulty = true
973
+ } else {
974
+ const fileSuitesWithNewTestsToProjects = new Map()
975
+ newTests.forEach(newTest => {
976
+ newTest._ddIsNew = true
977
+ if (isEarlyFlakeDetectionEnabled && newTest.expectedStatus !== 'skipped' && !newTest._ddIsModified) {
978
+ const fileSuite = getSuiteType(newTest, 'file')
979
+ if (!fileSuitesWithNewTestsToProjects.has(fileSuite)) {
980
+ fileSuitesWithNewTestsToProjects.set(fileSuite, getSuiteType(newTest, 'project'))
981
+ }
862
982
  }
863
- }
983
+ })
984
+
985
+ applyRetriesToTests(
986
+ fileSuitesWithNewTestsToProjects,
987
+ isNewTest,
988
+ ['_ddIsNew', '_ddIsEfdRetry'],
989
+ earlyFlakeDetectionNumRetries
990
+ )
864
991
  }
865
992
  }
866
993
 
@@ -953,6 +1080,9 @@ addHook({
953
1080
  const stepInfoByStepId = {}
954
1081
 
955
1082
  shimmer.wrap(workerPackage.WorkerMain.prototype, '_runTest', _runTest => async function (test) {
1083
+ if (test.expectedStatus === 'skipped') {
1084
+ return _runTest.apply(this, arguments)
1085
+ }
956
1086
  steps = []
957
1087
 
958
1088
  const {
@@ -1006,6 +1136,8 @@ addHook({
1006
1136
  })
1007
1137
 
1008
1138
  if (isRumActive) {
1139
+ // Give some time RUM to flush data, similar to what we do in selenium
1140
+ await new Promise(resolve => setTimeout(resolve, RUM_FLUSH_WAIT_TIME))
1009
1141
  const url = page.url()
1010
1142
  if (url) {
1011
1143
  const domain = new URL(url).hostname
@@ -1013,9 +1145,10 @@ addHook({
1013
1145
  name: 'datadog-ci-visibility-test-execution-id',
1014
1146
  value: '',
1015
1147
  domain,
1016
- expires: 0,
1017
1148
  path: '/'
1018
1149
  }])
1150
+ } else {
1151
+ log.error('RUM is active but page.url() is not available')
1019
1152
  }
1020
1153
  }
1021
1154
  }
@@ -5,6 +5,19 @@ const pathToRegExp = require('path-to-regexp')
5
5
  const shimmer = require('../../datadog-shimmer')
6
6
  const { addHook, channel } = require('./helpers/instrument')
7
7
 
8
+ const {
9
+ getRouterMountPaths,
10
+ joinPath,
11
+ getLayerMatchers,
12
+ setLayerMatchers,
13
+ isAppMounted,
14
+ setRouterMountPath,
15
+ extractMountPaths,
16
+ getRouteFullPaths,
17
+ wrapRouteMethodsAndPublish,
18
+ collectRoutesFromRouter
19
+ } = require('./helpers/router-helper')
20
+
8
21
  function isFastStar (layer, matchers) {
9
22
  return layer.regexp?.fast_star ?? matchers.some(matcher => matcher.path === '*')
10
23
  }
@@ -22,7 +35,6 @@ function createWrapRouterMethod (name) {
22
35
  const nextChannel = channel(`apm:${name}:middleware:next`)
23
36
  const routeAddedChannel = channel(`apm:${name}:route:added`)
24
37
 
25
- const layerMatchers = new WeakMap()
26
38
  const regexpCache = Object.create(null)
27
39
 
28
40
  function wrapLayerHandle (layer, original) {
@@ -31,7 +43,7 @@ function createWrapRouterMethod (name) {
31
43
  return shimmer.wrapFunction(original, original => function () {
32
44
  if (!enterChannel.hasSubscribers) return original.apply(this, arguments)
33
45
 
34
- const matchers = layerMatchers.get(layer)
46
+ const matchers = getLayerMatchers(layer)
35
47
  const lastIndex = arguments.length - 1
36
48
  const name = original._name || original.name
37
49
  const req = arguments[arguments.length > 3 ? 1 : 0]
@@ -78,7 +90,7 @@ function createWrapRouterMethod (name) {
78
90
  layer.handle = wrapLayerHandle(layer, layer.handle)
79
91
  }
80
92
 
81
- layerMatchers.set(layer, matchers)
93
+ setLayerMatchers(layer, matchers)
82
94
 
83
95
  if (layer.route) {
84
96
  METHODS.forEach(method => {
@@ -115,7 +127,7 @@ function createWrapRouterMethod (name) {
115
127
  return arg.map(pattern => ({
116
128
  path: pattern instanceof RegExp ? `(${pattern})` : pattern,
117
129
  test: layer => {
118
- const matchers = layerMatchers.get(layer)
130
+ const matchers = getLayerMatchers(layer)
119
131
  return !isFastStar(layer, matchers) &&
120
132
  !isFastSlash(layer, matchers) &&
121
133
  cachedPathToRegExp(pattern).test(layer.path)
@@ -134,12 +146,12 @@ function createWrapRouterMethod (name) {
134
146
  }
135
147
 
136
148
  function wrapMethod (original) {
137
- return shimmer.wrapFunction(original, original => function methodWithTrace (fn) {
149
+ return shimmer.wrapFunction(original, original => function methodWithTrace (fn, ...otherArgs) {
138
150
  let offset = 0
139
151
  if (this.stack) {
140
152
  offset = Array.isArray(this.stack) ? this.stack.length : 1
141
153
  }
142
- const router = original.apply(this, arguments)
154
+ const router = original.call(this, fn, ...otherArgs)
143
155
 
144
156
  if (typeof this.stack === 'function') {
145
157
  this.stack = [{ handle: this.stack }]
@@ -149,6 +161,51 @@ function createWrapRouterMethod (name) {
149
161
  routeAddedChannel.publish({ topOfStackFunc: methodWithTrace, layer: this.stack.at(-1) })
150
162
  }
151
163
 
164
+ // Publish only if this router was mounted by app.use() (prevents early '/sub/...')
165
+ if (routeAddedChannel.hasSubscribers && isAppMounted(this) && this.stack?.length > offset) {
166
+ // Handle nested router mounting for 'use' method
167
+ if (original.name === 'use' && otherArgs.length >= 1) {
168
+ const { mountPaths, startIdx } = extractMountPaths(fn)
169
+
170
+ if (mountPaths.length) {
171
+ const parentPaths = getRouterMountPaths(this)
172
+ const callArgs = [fn, ...otherArgs]
173
+
174
+ for (let i = startIdx; i < callArgs.length; i++) {
175
+ const nestedRouter = callArgs[i]
176
+
177
+ if (!nestedRouter || typeof nestedRouter !== 'function') continue
178
+
179
+ for (const parentPath of parentPaths) {
180
+ for (const normalizedMountPath of mountPaths) {
181
+ const fullMountPath = joinPath(parentPath, normalizedMountPath)
182
+ if (fullMountPath === null) continue
183
+
184
+ setRouterMountPath(nestedRouter, fullMountPath)
185
+ collectRoutesFromRouter(nestedRouter, fullMountPath)
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ const mountPaths = getRouterMountPaths(this)
193
+
194
+ if (mountPaths.length) {
195
+ const layer = this.stack.at(-1)
196
+
197
+ if (layer?.route) {
198
+ const route = layer.route
199
+
200
+ const fullPaths = mountPaths.flatMap(mountPath => getRouteFullPaths(route, mountPath))
201
+
202
+ wrapRouteMethodsAndPublish(route, fullPaths, (payload) => {
203
+ routeAddedChannel.publish(payload)
204
+ })
205
+ }
206
+ }
207
+ }
208
+
152
209
  if (this.stack.length > offset) {
153
210
  wrapStack(this.stack.slice(offset), extractMatchers(fn))
154
211
  }