dd-trace 6.20.0 → 6.21.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 (45) hide show
  1. package/index.d.ts +91 -3
  2. package/package.json +6 -7
  3. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +57 -4
  4. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  5. package/packages/datadog-instrumentations/src/jest.js +119 -1
  6. package/packages/datadog-instrumentations/src/mocha/utils.js +124 -36
  7. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +61 -34
  8. package/packages/datadog-instrumentations/src/mocha/worker.js +6 -3
  9. package/packages/datadog-instrumentations/src/playwright.js +3 -0
  10. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  11. package/packages/datadog-instrumentations/src/webdriverio.js +115 -15
  12. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +17 -0
  13. package/packages/datadog-plugin-mocha/src/index.js +216 -29
  14. package/packages/datadog-plugin-mocha/src/webdriverio-video.js +449 -0
  15. package/packages/dd-trace/src/config/supported-configurations.json +2 -2
  16. package/packages/dd-trace/src/debugger/constants.js +1 -0
  17. package/packages/dd-trace/src/debugger/devtools_client/index.js +5 -3
  18. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +17 -12
  19. package/packages/dd-trace/src/debugger/devtools_client/send.js +6 -2
  20. package/packages/dd-trace/src/debugger/devtools_client/worker-error.js +21 -0
  21. package/packages/dd-trace/src/debugger/index.js +24 -2
  22. package/packages/dd-trace/src/debugger/probe_sampler.js +108 -11
  23. package/packages/dd-trace/src/exporters/common/request.js +1 -0
  24. package/packages/dd-trace/src/exporters/common/url.js +1 -1
  25. package/packages/dd-trace/src/id.js +1 -4
  26. package/packages/dd-trace/src/llmobs/agent-manifest.js +296 -0
  27. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  28. package/packages/dd-trace/src/llmobs/sdk.js +23 -6
  29. package/packages/dd-trace/src/llmobs/span_processor.js +17 -6
  30. package/packages/dd-trace/src/llmobs/tagger.js +54 -1
  31. package/packages/dd-trace/src/llmobs/writers/base.js +3 -1
  32. package/packages/dd-trace/src/llmobs/writers/util.js +3 -1
  33. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +30 -1
  34. package/packages/dd-trace/src/openfeature/configuration_source.js +17 -2
  35. package/packages/dd-trace/src/openfeature/debug-logging-hook.js +25 -0
  36. package/packages/dd-trace/src/openfeature/flagging_provider.js +29 -0
  37. package/packages/dd-trace/src/openfeature/remote_config.js +9 -0
  38. package/packages/dd-trace/src/openfeature/writers/util.js +43 -4
  39. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +1 -1
  40. package/packages/dd-trace/src/otel-thread-ctx.js +2 -2
  41. package/packages/dd-trace/src/plugins/util/url.js +6 -1
  42. package/packages/dd-trace/src/profiling/profilers/wall.js +4 -10
  43. package/packages/dd-trace/src/proxy.js +2 -0
  44. package/packages/dd-trace/src/serverless.js +5 -0
  45. package/packages/dd-trace/src/storage-channels.js +112 -46
@@ -26,6 +26,7 @@ const shimmer = require('../../../datadog-shimmer')
26
26
  const testStartCh = channel('ci:mocha:test:start')
27
27
  const testFinishCh = channel('ci:mocha:test:finish')
28
28
  const testDiWaitCh = channel('ci:mocha:test:di:wait')
29
+ const testAfterEachFinishCh = channel('ci:mocha:after-each:finish')
29
30
  // after a test has failed, we'll publish to this channel
30
31
  const testRetryCh = channel('ci:mocha:test:retry')
31
32
  const errorCh = channel('ci:mocha:test:error')
@@ -54,6 +55,7 @@ const testsQuarantined = new Set()
54
55
  const testsStatuses = new Map()
55
56
  const efdRetryCountByTestFullName = new Map()
56
57
  const efdSlowAbortedTests = new Set()
58
+ const suiteHookFailureSuites = new Set()
57
59
  let dynamicAtrRetryCountByTest = new WeakMap()
58
60
  const attemptToFixExecutions = new Map()
59
61
  const isMochaWorker = !!getEnvironmentVariable('MOCHA_WORKER_ID')
@@ -343,6 +345,7 @@ function resetRunState (rootSuite) {
343
345
  testsStatuses.clear()
344
346
  efdRetryCountByTestFullName.clear()
345
347
  efdSlowAbortedTests.clear()
348
+ suiteHookFailureSuites.clear()
346
349
  dynamicAtrRetryCountByTest = new WeakMap()
347
350
  attemptToFixExecutions.clear()
348
351
  loggedAttemptToFixTests.clear()
@@ -379,7 +382,9 @@ function resetRunState (rootSuite) {
379
382
  delete test._ddTestFinishPublished
380
383
  delete test._ddIsFinalAttempt
381
384
  delete test._ddHookFailed
385
+ delete test._ddSuppressSyntheticTestLifecycle
382
386
  delete test._ddPendingRetry
387
+ delete test._ddPendingHookFailureFinish
383
388
  delete test._ddReporterStartFailed
384
389
  delete test._ddReporterTerminalFailed
385
390
  originalTests.push(test)
@@ -439,6 +444,21 @@ function recordTestAttempt (testsByFullName, test) {
439
444
  }
440
445
  }
441
446
 
447
+ function recordRetryTrackedAttempt (test) {
448
+ const {
449
+ _ddIsAttemptToFix: isAttemptToFix,
450
+ _ddIsModified: isModified,
451
+ _ddIsNew: isNew,
452
+ } = test
453
+
454
+ if (isNew) {
455
+ recordTestAttempt(newTests, test)
456
+ }
457
+ if (!isAttemptToFix && (isNew || isModified)) {
458
+ recordTestAttempt(efdTests, test)
459
+ }
460
+ }
461
+
442
462
  function getTestStatus (test) {
443
463
  if (test.isPending()) {
444
464
  return 'skip'
@@ -476,6 +496,15 @@ function startTestFinish (test) {
476
496
  return true
477
497
  }
478
498
 
499
+ function hasHandledHookFailure (test) {
500
+ return test._ddHookFailed && (test._ddTestFinishPublished || test._ddPendingHookFailureFinish)
501
+ }
502
+
503
+ function suppressSyntheticTestLifecycle (test) {
504
+ test._ddSuppressSyntheticTestLifecycle = true
505
+ test._ddTestFinishPublished = true
506
+ }
507
+
479
508
  /**
480
509
  * Copies Test Management metadata from Mocha's original runnable to its native retry clone.
481
510
  * @param {{
@@ -571,6 +600,14 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
571
600
  }
572
601
  }
573
602
 
603
+ const failedTestReplayPromise = (isTestHook || this.type === 'test') &&
604
+ test._retriedTest?._ddFailedTestReplayPromise
605
+ if (failedTestReplayPromise) {
606
+ delete test._retriedTest._ddFailedTestReplayPromise
607
+ const runRunnable = () => run.apply(this, args)
608
+ return failedTestReplayPromise.then(runRunnable, runRunnable)
609
+ }
610
+
574
611
  return run.apply(this, args)
575
612
  })
576
613
  return RunnablePackage
@@ -578,6 +615,8 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
578
615
 
579
616
  function getOnTestHandler (isMain) {
580
617
  return function (test) {
618
+ if (test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test)) return
619
+
581
620
  const testStartLine = testToStartLine.get(test)
582
621
 
583
622
  // This may be a retry. If this is the case, `test.fn` is already wrapped,
@@ -586,6 +625,12 @@ function getOnTestHandler (isMain) {
586
625
 
587
626
  inheritDatadogPropertiesFromRetriedTest(test)
588
627
 
628
+ if (isInFailedSuite(test.parent)) {
629
+ recordRetryTrackedAttempt(test)
630
+ suppressSyntheticTestLifecycle(test)
631
+ return
632
+ }
633
+
589
634
  const {
590
635
  file: testSuiteAbsolutePath,
591
636
  title,
@@ -636,13 +681,8 @@ function getOnTestHandler (isMain) {
636
681
  loggedAttemptToFixTests
637
682
  )
638
683
  }
639
- // We want to store the result of the new tests
640
- if (isNew) {
641
- recordTestAttempt(newTests, test)
642
- }
643
- if (!isAttemptToFix && (isNew || isModified)) {
644
- recordTestAttempt(efdTests, test)
645
- }
684
+ // Keep retry accounting aligned with Mocha's runner failure counts.
685
+ recordRetryTrackedAttempt(test)
646
686
 
647
687
  if (!isAttemptToFix && isDisabled) {
648
688
  if (!originalPendingByTest.has(test)) {
@@ -657,6 +697,14 @@ function getOnTestHandler (isMain) {
657
697
  }
658
698
  }
659
699
 
700
+ function isInFailedSuite (suite) {
701
+ while (suite) {
702
+ if (suiteHookFailureSuites.has(suite)) return true
703
+ suite = suite.parent
704
+ }
705
+ return false
706
+ }
707
+
660
708
  function getFinalStatus ({
661
709
  status,
662
710
  hasFailedAllRetries,
@@ -842,7 +890,7 @@ function getTestFinishInfo (test, status, config, error) {
842
890
 
843
891
  function getOnTestEndHandler (config, finalAttemptHandlers) {
844
892
  return async function (test) {
845
- if (test._ddShouldSkipEfdRetry) {
893
+ if (test._ddShouldSkipEfdRetry || test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test)) {
846
894
  return
847
895
  }
848
896
  const shouldWaitForHitProbe = test._retriedTest?._ddShouldWaitForHitProbe
@@ -880,24 +928,33 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
880
928
  finalAttemptHandlers?.onStart?.(test)
881
929
  }
882
930
 
883
- if (shouldWaitForHitProbe) {
884
- await waitForHitProbe()
885
- }
931
+ const finish = () => {
932
+ if (shouldFinishTest) {
933
+ test._ddTestFinishPublished = true
934
+ testFinishCh.publish({
935
+ status,
936
+ hasBeenRetried: isMochaRetry(test),
937
+ isLastRetry: getIsLastRetry(test),
938
+ ...testFinishInfo,
939
+ ...ctx.currentStore,
940
+ })
941
+ }
886
942
 
887
- if (shouldFinishTest) {
888
- test._ddTestFinishPublished = true
889
- testFinishCh.publish({
890
- status,
891
- hasBeenRetried: isMochaRetry(test),
892
- isLastRetry: getIsLastRetry(test),
893
- ...testFinishInfo,
894
- ...ctx.currentStore,
895
- })
943
+ if (isFinalAttempt) {
944
+ finalAttemptHandlers?.onFinish?.(test)
945
+ }
896
946
  }
897
947
 
898
- if (isFinalAttempt) {
899
- finalAttemptHandlers?.onFinish?.(test)
948
+ if (shouldWaitForHitProbe) {
949
+ const waitForHitProbePromise = waitForHitProbe()
950
+ if (shouldFinishTest && config.isTestFailureVideosEnabled && !test.isPending()) {
951
+ // Mocha does not await test-end listeners. Its hookUp continuation must wait even without teardown hooks.
952
+ test._ddDeferredHookEnd = { waitForHitProbePromise, publishTestFinish: finish }
953
+ return
954
+ }
955
+ await waitForHitProbePromise
900
956
  }
957
+ finish()
901
958
  }
902
959
  }
903
960
 
@@ -978,17 +1035,30 @@ function finishDeferredHookEnd (test) {
978
1035
  * @returns {unknown}
979
1036
  */
980
1037
  function runFailedTestReplayHookUpCallback (fn, test, failedTestReplayPromise, hookThis, args) {
1038
+ if (test._ddPendingHookFailureFinish) {
1039
+ // Mocha restarts teardown at the failed suite's parent. Only the last hookUp completes the attempt.
1040
+ const failedSuite = /** @type {import('mocha').Suite|undefined} */ (args[1])
1041
+ if (args[0] && failedSuite?.parent) return fn.apply(hookThis, /** @type {unknown[]} */ (args))
1042
+ const finish = test._ddPendingHookFailureFinish
1043
+ delete test._ddPendingHookFailureFinish
1044
+ finish()
1045
+ }
981
1046
  const pendingRetry = test._ddPendingRetry
982
1047
  if (pendingRetry) {
983
1048
  delete test._ddPendingRetry
984
1049
  failedTestReplayPromise = pendingRetry()
985
1050
  }
1051
+ const continueAfterCapture = () => {
1052
+ const context = { onDone: () => fn.apply(hookThis, args), isWaiting: false }
1053
+ testAfterEachFinishCh.publish(context)
1054
+ return context.isWaiting ? undefined : context.onDone()
1055
+ }
986
1056
  const continueAfterProbe = () => {
987
1057
  const deferredHookEndPromise = finishDeferredHookEnd(test)
988
1058
  if (deferredHookEndPromise) {
989
- return deferredHookEndPromise.then(() => fn.apply(hookThis, args), () => fn.apply(hookThis, args))
1059
+ return deferredHookEndPromise.then(continueAfterCapture, continueAfterCapture)
990
1060
  }
991
- return fn.apply(hookThis, args)
1061
+ return continueAfterCapture()
992
1062
  }
993
1063
 
994
1064
  if (failedTestReplayPromise) {
@@ -1037,15 +1107,23 @@ function getOnFailHandler (isMain, config) {
1037
1107
  const testFile = testOrHook.file
1038
1108
  let test = testOrHook
1039
1109
  const isHook = testOrHook.type === 'hook'
1110
+ const isBeforeEachHook = isHook && testOrHook.parent?._beforeEach?.includes(testOrHook)
1111
+ const isAfterEachHook = isHook && testOrHook.parent?._afterEach?.includes(testOrHook)
1112
+ const isTestHook = isBeforeEachHook || isAfterEachHook
1113
+ if (isHook && !isTestHook && testOrHook.parent) {
1114
+ suiteHookFailureSuites.add(testOrHook.parent)
1115
+ }
1040
1116
  if (isHook && testOrHook.ctx) {
1041
1117
  test = testOrHook.ctx.currentTest
1042
1118
  }
1043
1119
  let testContext
1044
1120
  if (test) {
1121
+ if (!isHook && (test._ddSuppressSyntheticTestLifecycle || hasHandledHookFailure(test))) return
1122
+
1045
1123
  testContext = getTestContext(test)
1046
1124
  }
1047
1125
  if (testContext) {
1048
- if (isHook && startTestFinish(test)) {
1126
+ if (isTestHook && startTestFinish(test)) {
1049
1127
  delete test._ddPendingRetry
1050
1128
  const hookError = new Error(`${testOrHook.fullTitle()}: ${err.message}`, { cause: err })
1051
1129
  hookError.name = err.name
@@ -1054,17 +1132,27 @@ function getOnFailHandler (isMain, config) {
1054
1132
  errorCh.runStores(testContext, () => {})
1055
1133
  // Mocha marks the hook failed, so record the test failure before computing final metadata.
1056
1134
  test._ddHookFailed = true
1057
- const testFinishInfo = getTestFinishInfo(test, 'fail', config, hookError)
1058
- if (testFinishInfo.finalStatus !== undefined) {
1059
- test._ddIsFinalAttempt = true
1135
+ if (isBeforeEachHook && testOrHook.parent) {
1136
+ suiteHookFailureSuites.add(testOrHook.parent)
1137
+ }
1138
+ const finish = () => {
1139
+ const testFinishInfo = getTestFinishInfo(test, 'fail', config, testContext.err)
1140
+ if (testFinishInfo.finalStatus !== undefined) {
1141
+ test._ddIsFinalAttempt = true
1142
+ }
1143
+ test._ddTestFinishPublished = true
1144
+ testFinishCh.publish({
1145
+ status: 'fail',
1146
+ hasBeenRetried: isMochaRetry(test),
1147
+ ...testFinishInfo,
1148
+ ...testContext.currentStore,
1149
+ })
1150
+ }
1151
+ if (config.isTestFailureVideosEnabled) {
1152
+ test._ddPendingHookFailureFinish = finish
1153
+ } else {
1154
+ finish()
1060
1155
  }
1061
- test._ddTestFinishPublished = true
1062
- testFinishCh.publish({
1063
- status: 'fail',
1064
- hasBeenRetried: isMochaRetry(test),
1065
- ...testFinishInfo,
1066
- ...testContext.currentStore,
1067
- })
1068
1156
  } else {
1069
1157
  testContext.err = err
1070
1158
  errorCh.runStores(testContext, () => {})
@@ -1105,7 +1193,7 @@ function getOnTestRetryHandler (config) {
1105
1193
  test._retries = dynamicCount
1106
1194
  }
1107
1195
 
1108
- if (config.isFlakyTestRetriesEnabled && getAfterEachHooks(test).length) {
1196
+ if ((config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) && getAfterEachHooks(test).length) {
1109
1197
  // Mocha queues retries before afterEach; a hook failure can still cancel the retry.
1110
1198
  test._ddPendingRetry = () => publishTestRetry(test, err, config)
1111
1199
  return
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { clearTimeout, setTimeout } = require('node:timers')
4
+
3
5
  const {
4
6
  createWebdriverioWorkerMessage,
5
7
  WEBDRIVERIO_WORKER_ENV,
@@ -11,6 +13,9 @@ const { FINAL_FLUSH_TIMEOUT } = require('../../../dd-trace/src/ci-visibility/fin
11
13
  const SCREENSHOT_UPLOAD = 'dd:test-optimization:webdriverio:screenshot:upload'
12
14
  const SCREENSHOT_UPLOAD_RESPONSE = 'dd:test-optimization:webdriverio:screenshot:upload:response'
13
15
  const SCREENSHOT_UPLOAD_TIMEOUT_MS = FINAL_FLUSH_TIMEOUT + 5000
16
+ const VIDEO_UPLOAD = 'dd:test-optimization:webdriverio:video:upload'
17
+ const VIDEO_UPLOAD_RESPONSE = 'dd:test-optimization:webdriverio:video:upload:response'
18
+ const VIDEO_UPLOAD_TIMEOUT_MS = 5 * FINAL_FLUSH_TIMEOUT + 5000
14
19
 
15
20
  /**
16
21
  * Sends a message over WebdriverIO's worker IPC envelope.
@@ -34,95 +39,113 @@ function sendWebdriverioWorkerMessage (message, onError, onDone) {
34
39
  })
35
40
  }
36
41
 
37
- let screenshotUploadRequestId = 0
38
- const screenshotUploadRequests = new Map()
42
+ let mediaUploadRequestId = 0
43
+ const mediaUploadRequests = new Map()
39
44
 
40
45
  /**
41
- * Removes shared screenshot response listeners when there are no pending requests.
46
+ * Removes shared media response listeners when there are no pending requests.
42
47
  *
43
48
  */
44
- function removeScreenshotUploadListeners () {
45
- if (screenshotUploadRequests.size !== 0) return
49
+ function removeMediaUploadListeners () {
50
+ if (mediaUploadRequests.size !== 0) return
46
51
 
47
- process.off('message', onScreenshotUploadResponse)
48
- process.off('disconnect', onScreenshotUploadDisconnect)
52
+ process.off('message', onMediaUploadResponse)
53
+ process.off('disconnect', onMediaUploadDisconnect)
49
54
  }
50
55
 
51
56
  /**
52
- * Completes one pending screenshot upload request.
57
+ * Completes one pending media upload request.
53
58
  *
54
59
  * @param {string} requestId
55
60
  * @param {Error} [error]
56
61
  */
57
- function finishScreenshotUploadRequest (requestId, error) {
58
- const request = screenshotUploadRequests.get(requestId)
62
+ function finishMediaUploadRequest (requestId, error) {
63
+ const request = mediaUploadRequests.get(requestId)
59
64
  if (!request) return
60
65
 
61
- screenshotUploadRequests.delete(requestId)
66
+ mediaUploadRequests.delete(requestId)
62
67
  clearTimeout(request.timeout)
63
- removeScreenshotUploadListeners()
68
+ removeMediaUploadListeners()
64
69
  request.onDone(error)
65
70
  }
66
71
 
67
72
  /**
68
- * Dispatches one coordinator screenshot response to its pending request.
73
+ * Dispatches one coordinator media response to its pending request.
69
74
  *
70
75
  * @param {object} message
71
76
  */
72
- function onScreenshotUploadResponse (message) {
73
- if (message?.name !== SCREENSHOT_UPLOAD_RESPONSE) return
77
+ function onMediaUploadResponse (message) {
78
+ if (message?.name !== SCREENSHOT_UPLOAD_RESPONSE && message?.name !== VIDEO_UPLOAD_RESPONSE) return
74
79
 
75
80
  const { error: errorMessage, requestId } = message.content || {}
76
81
  if (!requestId) return
77
82
 
78
- finishScreenshotUploadRequest(requestId, errorMessage ? new Error(errorMessage) : undefined)
83
+ finishMediaUploadRequest(requestId, errorMessage ? new Error(errorMessage) : undefined)
79
84
  }
80
85
 
81
86
  /**
82
- * Fails every pending screenshot upload after coordinator disconnect.
87
+ * Fails every pending media upload after coordinator disconnect.
83
88
  *
84
89
  */
85
- function onScreenshotUploadDisconnect () {
86
- for (const requestId of screenshotUploadRequests.keys()) {
87
- finishScreenshotUploadRequest(
90
+ function onMediaUploadDisconnect () {
91
+ for (const requestId of mediaUploadRequests.keys()) {
92
+ finishMediaUploadRequest(
88
93
  requestId,
89
- new Error('WebdriverIO coordinator disconnected during screenshot upload')
94
+ new Error('WebdriverIO coordinator disconnected during media upload')
90
95
  )
91
96
  }
92
97
  }
93
98
 
94
99
  /**
95
- * Requests one screenshot upload from the WebdriverIO coordinator.
100
+ * Requests one media upload from the WebdriverIO coordinator.
96
101
  *
97
- * @param {object} content - Screenshot upload metadata
102
+ * @param {object} content - Upload metadata
103
+ * @param {'screenshot'|'video'} kind
98
104
  * @param {(error?: Error) => void} onDone - Upload completion callback
99
105
  */
100
- function requestWebdriverioScreenshotUpload (content, onDone) {
101
- const requestId = `${process.pid}-${++screenshotUploadRequestId}`
106
+ function requestWebdriverioMediaUpload (content, kind, onDone) {
107
+ const requestId = `${process.pid}-${++mediaUploadRequestId}`
102
108
  const timeout = setTimeout(() => {
103
- finishScreenshotUploadRequest(requestId, new Error('WebdriverIO screenshot upload timed out'))
104
- }, SCREENSHOT_UPLOAD_TIMEOUT_MS)
109
+ finishMediaUploadRequest(requestId, new Error(`WebdriverIO ${kind} upload timed out`))
110
+ }, kind === 'video' ? VIDEO_UPLOAD_TIMEOUT_MS : SCREENSHOT_UPLOAD_TIMEOUT_MS)
105
111
  timeout.unref?.()
106
- if (screenshotUploadRequests.size === 0) {
107
- process.on('message', onScreenshotUploadResponse)
108
- process.once('disconnect', onScreenshotUploadDisconnect)
112
+ if (mediaUploadRequests.size === 0) {
113
+ process.on('message', onMediaUploadResponse)
114
+ process.once('disconnect', onMediaUploadDisconnect)
109
115
  }
110
- screenshotUploadRequests.set(requestId, { onDone, timeout })
116
+ mediaUploadRequests.set(requestId, { onDone, timeout })
111
117
  sendWebdriverioWorkerMessage({
112
118
  origin: 'datadog',
113
- name: SCREENSHOT_UPLOAD,
119
+ name: kind === 'video' ? VIDEO_UPLOAD : SCREENSHOT_UPLOAD,
114
120
  content: { ...content, requestId },
115
- }, error => finishScreenshotUploadRequest(
121
+ }, error => finishMediaUploadRequest(
116
122
  requestId,
117
- error || new Error('WebdriverIO screenshot upload IPC failed')
123
+ error || new Error(`WebdriverIO ${kind} upload IPC failed`)
118
124
  ))
119
125
  }
120
126
 
127
+ /**
128
+ * @param {object} content
129
+ * @param {(error?: Error) => void} onDone
130
+ */
131
+ function requestWebdriverioScreenshotUpload (content, onDone) {
132
+ requestWebdriverioMediaUpload(content, 'screenshot', onDone)
133
+ }
134
+
135
+ /**
136
+ * @param {object} content
137
+ * @param {(error?: Error) => void} onDone
138
+ */
139
+ function requestWebdriverioVideoUpload (content, onDone) {
140
+ requestWebdriverioMediaUpload(content, 'video', onDone)
141
+ }
142
+
121
143
  module.exports = {
122
144
  CONFIGURATION_REQUEST: 'dd:test-optimization:webdriverio:configuration:request',
123
145
  CONFIGURATION_RESPONSE: 'dd:test-optimization:webdriverio:configuration:response',
124
146
  createWebdriverioWorkerMessage,
125
147
  requestWebdriverioScreenshotUpload,
148
+ requestWebdriverioVideoUpload,
126
149
  SCREENSHOT_UPLOAD,
127
150
  SCREENSHOT_UPLOAD_RESPONSE,
128
151
  SCREENSHOT_UPLOAD_TIMEOUT_MS,
@@ -130,6 +153,10 @@ module.exports = {
130
153
  SUITE_FINISH: 'dd:test-optimization:webdriverio:test-suite:finish',
131
154
  WORKER_READY: 'dd:test-optimization:webdriverio:worker:ready',
132
155
  WORKER_READY_RESPONSE: 'dd:test-optimization:webdriverio:worker:ready:response',
156
+ VIDEO_UPLOAD_FLUSH: 'dd:test-optimization:webdriverio:video:flush',
157
+ VIDEO_UPLOAD,
158
+ VIDEO_UPLOAD_RESPONSE,
159
+ VIDEO_UPLOAD_TIMEOUT_MS,
133
160
  WEBDRIVERIO_WORKER_ENV,
134
161
  WEBDRIVERIO_WORKER_EVENT,
135
162
  WEBDRIVERIO_WORKER_ORIGIN,
@@ -416,7 +416,7 @@ function wrapMochaRun (Mocha, frameworkVersion) {
416
416
  })
417
417
  }
418
418
  filterSkippedFiles(runner, skippedFiles)
419
- if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled) {
419
+ if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) {
420
420
  patchFailedTestReplayHookUp(runner.constructor)
421
421
  }
422
422
  configurationReady = true
@@ -453,7 +453,7 @@ addHook({
453
453
  if (!workerFinishCh.hasSubscribers) {
454
454
  return run.apply(this, args)
455
455
  }
456
- if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled) {
456
+ if (isFailedTestReplayEnabled() || config.isFlakyTestRetriesEnabled || config.isTestFailureVideosEnabled) {
457
457
  patchFailedTestReplayHookUp(Runner)
458
458
  }
459
459
  if (isWebdriverioWorker) {
@@ -469,7 +469,10 @@ addHook({
469
469
  const onRunDone = args[0]
470
470
  if (isWebdriverioWorker && typeof onRunDone === 'function') {
471
471
  args[0] = (...onRunDoneArgs) => {
472
- finishWebdriverioWorker(this, () => onRunDone(...onRunDoneArgs))
472
+ const adjustedFailures = Number.isFinite(this.failures)
473
+ ? Math.max(0, this.failures)
474
+ : onRunDoneArgs[0]
475
+ finishWebdriverioWorker(this, () => onRunDone(adjustedFailures, ...onRunDoneArgs.slice(1)))
473
476
  }
474
477
  } else {
475
478
  // Flush after the worker finishes its Mocha run, including grouped spec files.
@@ -2657,11 +2657,14 @@ function instrumentWorkerMainMethods (workerMain) {
2657
2657
  }
2658
2658
 
2659
2659
  // In cases where there is no afterEach hook with _ddHook, we need to add one
2660
+ // Playwright 1.64 builds the hook step stack from hook.location, so an undefined
2661
+ // location ends up as [undefined] and breaks trace writing
2660
2662
  if (!existAfterEachHook) {
2661
2663
  test.parent._hooks.push({
2662
2664
  type: 'afterEach',
2663
2665
  fn: cleanupRumPages,
2664
2666
  title: 'afterEach hook',
2667
+ location: test.location,
2665
2668
  _ddHook: true,
2666
2669
  })
2667
2670
  }
@@ -2239,8 +2239,6 @@ addHook({
2239
2239
  versions: ['>=5.0.0'],
2240
2240
  filePattern: 'dist/chunks/index.*',
2241
2241
  }, (vitestPackage, frameworkVersion) => {
2242
- if (!getVitestExport(vitestPackage)) return vitestPackage
2243
-
2244
2242
  wrapVitestInternals(vitestPackage, frameworkVersion)
2245
2243
 
2246
2244
  const typecheckPoolWorker = getTypecheckPoolWorkerExport(vitestPackage)