dd-trace 6.17.0 → 6.18.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 (70) hide show
  1. package/index.d.ts +52 -9
  2. package/package.json +1 -1
  3. package/packages/datadog-instrumentations/src/cucumber.js +10 -5
  4. package/packages/datadog-instrumentations/src/cypress-config.js +27 -15
  5. package/packages/datadog-instrumentations/src/helpers/bundler-register.js +6 -0
  6. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +2 -1
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +0 -6
  8. package/packages/datadog-instrumentations/src/helpers/register.js +20 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +41 -4
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +15 -13
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +43 -0
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +13 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +28 -3
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +4 -0
  15. package/packages/datadog-instrumentations/src/jest.js +27 -3
  16. package/packages/datadog-instrumentations/src/mocha/main.js +7 -5
  17. package/packages/datadog-instrumentations/src/mocha/utils.js +8 -8
  18. package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -4
  19. package/packages/datadog-instrumentations/src/playwright.js +47 -6
  20. package/packages/datadog-instrumentations/src/undici.js +7 -3
  21. package/packages/datadog-instrumentations/src/vitest-main.js +91 -20
  22. package/packages/datadog-instrumentations/src/webdriverio.js +28 -12
  23. package/packages/datadog-instrumentations/src/ws.js +41 -1
  24. package/packages/datadog-plugin-cucumber/src/index.js +3 -4
  25. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +44 -14
  26. package/packages/datadog-plugin-cypress/src/index.js +3 -1
  27. package/packages/datadog-plugin-jest/src/index.js +3 -4
  28. package/packages/datadog-plugin-mocha/src/index.js +3 -4
  29. package/packages/datadog-plugin-playwright/src/index.js +11 -6
  30. package/packages/datadog-plugin-vitest/src/index.js +3 -4
  31. package/packages/datadog-plugin-ws/src/producer.js +1 -1
  32. package/packages/datadog-shimmer/src/shimmer.js +14 -10
  33. package/packages/datadog-turbopack/index.js +4 -1
  34. package/packages/datadog-turbopack/src/loader.js +6 -2
  35. package/packages/dd-trace/src/agent/info.js +5 -4
  36. package/packages/dd-trace/src/config/generated-config-types.d.ts +15 -15
  37. package/packages/dd-trace/src/config/index.js +7 -6
  38. package/packages/dd-trace/src/config/supported-configurations.json +11 -0
  39. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -1
  40. package/packages/dd-trace/src/debugger/devtools_client/send.js +1 -1
  41. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +5 -3
  42. package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
  43. package/packages/dd-trace/src/debugger/index.js +1 -1
  44. package/packages/dd-trace/src/evp_proxy/constants.js +2 -0
  45. package/packages/dd-trace/src/evp_proxy/direct.js +17 -13
  46. package/packages/dd-trace/src/evp_proxy/discovery.js +14 -10
  47. package/packages/dd-trace/src/evp_proxy/path.js +21 -7
  48. package/packages/dd-trace/src/exporters/common/url.js +33 -2
  49. package/packages/dd-trace/src/llmobs/prompts/manager.js +10 -2
  50. package/packages/dd-trace/src/llmobs/prompts/prompt.js +77 -11
  51. package/packages/dd-trace/src/llmobs/tagger.js +9 -4
  52. package/packages/dd-trace/src/openfeature/configuration_source.js +4 -1
  53. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -3
  54. package/packages/dd-trace/src/openfeature/index.js +6 -2
  55. package/packages/dd-trace/src/openfeature/writers/base.js +90 -17
  56. package/packages/dd-trace/src/openfeature/writers/exposures.js +4 -0
  57. package/packages/dd-trace/src/openfeature/writers/util.js +104 -33
  58. package/packages/dd-trace/src/opentracing/tracer.js +3 -3
  59. package/packages/dd-trace/src/plugin_manager.js +27 -8
  60. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -1
  61. package/packages/dd-trace/src/plugins/util/test.js +9 -4
  62. package/packages/dd-trace/src/proxy.js +2 -2
  63. package/packages/dd-trace/src/telemetry/send-data.js +2 -1
  64. package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
  65. package/packages/dd-trace/src/tracer.js +5 -4
  66. package/packages/datadog-instrumentations/src/azure-cosmos.js +0 -7
  67. package/packages/datadog-instrumentations/src/bullmq.js +0 -11
  68. package/packages/datadog-instrumentations/src/langchain.js +0 -7
  69. package/packages/datadog-instrumentations/src/langgraph.js +0 -7
  70. package/packages/datadog-instrumentations/src/mercurius.js +0 -11
@@ -63,6 +63,7 @@ const baseReporterWaitForSyncCh = tracingChannel('orchestrion:@wdio/runner:BaseR
63
63
  const runnerRunCh = tracingChannel('orchestrion:@wdio/runner:Runner_run')
64
64
  const executeAsyncCh = tracingChannel('orchestrion:@wdio/utils:executeAsync')
65
65
  const launcherStartInstanceCh = tracingChannel('orchestrion:@wdio/cli:Launcher_startInstance')
66
+ const configParserShardCh = tracingChannel('orchestrion:@wdio/config:ConfigParser_shard')
66
67
  const localRunnerRunCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_run')
67
68
  const localRunnerShutdownCh = tracingChannel('orchestrion:@wdio/local-runner:LocalRunner_shutdown')
68
69
  const testFrameworkFnWrapperCh = tracingChannel('orchestrion:@wdio/utils:testFrameworkFnWrapper')
@@ -87,6 +88,7 @@ if (loadCh.hasSubscribers) {
87
88
  }
88
89
 
89
90
  const coordinatorStates = new WeakMap()
91
+ const emptyShardConfigurations = new WeakSet()
90
92
  const localRunnerVersions = new WeakMap()
91
93
  const rumBrowsers = new Set()
92
94
  const rumCorrelationBrowsers = new Set()
@@ -936,6 +938,7 @@ function waitForRumTestStart (context) {
936
938
  * @property {string} framework
937
939
  * @property {string|undefined} rootDir
938
940
  * @property {typeof process.env|undefined} runnerEnv
941
+ * @property {{total: number}|undefined} shard
939
942
  */
940
943
 
941
944
  /**
@@ -1806,17 +1809,18 @@ function getSessionStatus (state) {
1806
1809
  }
1807
1810
 
1808
1811
  /**
1809
- * Returns whether every started worker reported that it discovered no tests.
1812
+ * Explains a skipped session using the workers' discovery results.
1810
1813
  *
1811
1814
  * @param {CoordinatorState} state
1812
1815
  */
1813
- function isExpectedEmptySession (state) {
1814
- if (state.workers.size === 0) return false
1815
-
1816
+ function getTestSessionEmptyReason (state) {
1817
+ if (state.workers.size === 0 && emptyShardConfigurations.has(getRunnerConfiguration(state.localRunner))) {
1818
+ return 'zero_test_shard'
1819
+ }
1816
1820
  for (const workerRecord of state.workers) {
1817
- if (workerRecord.hasTests !== false) return false
1821
+ if (workerRecord.hasTests !== false) return 'all_tests_skipped'
1818
1822
  }
1819
- return true
1823
+ return 'zero_tests'
1820
1824
  }
1821
1825
 
1822
1826
  /**
@@ -1832,9 +1836,9 @@ function finishCoordinator (state, error, onDone) {
1832
1836
  return
1833
1837
  }
1834
1838
  if (!state.sessionStarted) {
1835
- if (!error && getSessionStatus(state) !== 'fail') {
1836
- onDone()
1837
- return
1839
+ if (!error && state.workers.size === 0 &&
1840
+ !emptyShardConfigurations.has(getRunnerConfiguration(state.localRunner))) {
1841
+ error = new Error('No test files were found.')
1838
1842
  }
1839
1843
  initializeCoordinator(state, () => finishCoordinator(state, error, onDone))
1840
1844
  return
@@ -1861,7 +1865,7 @@ function finishCoordinator (state, error, onDone) {
1861
1865
  const status = error ? 'fail' : getSessionStatus(state)
1862
1866
  testSessionFinishCh.publish({
1863
1867
  status,
1864
- isExpectedEmptySession: status === 'skip' && isExpectedEmptySession(state),
1868
+ testSessionEmptyReason: status === 'skip' ? getTestSessionEmptyReason(state) : undefined,
1865
1869
  error,
1866
1870
  isEarlyFlakeDetectionEnabled: state.configuration.isEarlyFlakeDetectionEnabled,
1867
1871
  isEarlyFlakeDetectionFaulty: state.configuration.isEarlyFlakeDetectionFaulty,
@@ -1976,6 +1980,17 @@ jasmineAdapterInitCh.subscribe({
1976
1980
 
1977
1981
  // dc-polyfill supports partial tracing-channel subscribers, unlike the Node.js type definition.
1978
1982
  // @ts-expect-error
1983
+ configParserShardCh.subscribe({
1984
+ end (context) {
1985
+ const config = context.self._config
1986
+ if (testFinishCh.hasSubscribers && config?.shard?.total > 1 &&
1987
+ context.arguments[0].length > 0 && context.result?.length === 0) {
1988
+ emptyShardConfigurations.add(config)
1989
+ }
1990
+ },
1991
+ })
1992
+
1993
+ // @ts-expect-error See the partial tracing-channel subscriber above.
1979
1994
  launcherStartInstanceCh.subscribe({
1980
1995
  start (context) {
1981
1996
  const localRunner = context.self?.runner
@@ -2050,10 +2065,11 @@ localRunnerRunCh.subscribe({
2050
2065
  // @ts-expect-error See the partial tracing-channel subscriber above.
2051
2066
  localRunnerShutdownCh.subscribe({
2052
2067
  asyncEnd (context) {
2053
- const state = coordinatorStates.get(context.self)
2054
- if (!state) {
2068
+ const runnerConfiguration = getRunnerConfiguration(context.self)
2069
+ if (!testFinishCh.hasSubscribers || !SUPPORTED_FRAMEWORK_ADAPTERS.has(runnerConfiguration?.framework)) {
2055
2070
  return
2056
2071
  }
2072
+ const state = getCoordinatorState(context.self)
2057
2073
 
2058
2074
  // Orchestrion uses the callback for the matching settlement path to delay LocalRunner.shutdown.
2059
2075
  const waitForCoordinator = onDone => {
@@ -94,9 +94,22 @@ function wrapSend (send) {
94
94
  return send.apply(this, arguments)
95
95
  }
96
96
 
97
+ const socket = this._sender?._socket
98
+ // No socket is attached before the handshake completes or after it is torn down.
99
+ if (!socket) {
100
+ return send.apply(this, arguments)
101
+ }
102
+
97
103
  const [data, options, cb] = arguments
98
104
 
99
- const ctx = { data, socket: this._sender?._socket }
105
+ // `ws` stringifies numbers, then spreads `options` over its own inferred default, so an
106
+ // explicitly passed `binary` wins even when it is nullish and the frame goes out as text.
107
+ const payload = typeof data === 'number' ? data.toString() : data
108
+ const binary = options != null && Object.hasOwn(options, 'binary')
109
+ ? Boolean(options.binary)
110
+ : typeof payload !== 'string'
111
+ const byteLength = sentDataLength(payload)
112
+ const ctx = { data, binary, socket, byteLength }
100
113
 
101
114
  return typeof cb === 'function'
102
115
  ? producerCh.traceCallback(send, undefined, ctx, this, data, options, cb)
@@ -241,6 +254,33 @@ addHook({
241
254
  return Receiver
242
255
  })
243
256
 
257
+ /**
258
+ * Byte length of an outgoing payload, following the coercion `ws` applies in `Sender.send`:
259
+ * strings and blobs are measured directly and everything else goes through `toBuffer()`, which
260
+ * yields one byte per element for plain arrays rather than concatenating them. Blob-likes carry
261
+ * neither a `byteLength` nor an array shape, so they are matched last to keep the common
262
+ * buffer path first.
263
+ *
264
+ * @param {WebSocketMessageData | number | undefined} data
265
+ */
266
+ function sentDataLength (data) {
267
+ if (typeof data === 'string') {
268
+ return Buffer.byteLength(data)
269
+ }
270
+ // An array is one byte per element even when it carries an unrelated `byteLength`.
271
+ if (Array.isArray(data)) {
272
+ return data.length
273
+ }
274
+ // Covers views as well as `ArrayBuffer` and `SharedArrayBuffer`.
275
+ if (typeof (/** @type {{ byteLength?: unknown }} */ (data)?.byteLength) === 'number') {
276
+ return /** @type {{ byteLength: number }} */ (data).byteLength
277
+ }
278
+ // Blobs and files, detected the way `ws` does: structurally, so shimmed and cross-realm
279
+ // implementations it accepts are measured instead of silently reported as empty.
280
+ const { size } = /** @type {{ size?: unknown }} */ (data) ?? {}
281
+ return typeof size === 'number' ? size : 0
282
+ }
283
+
244
284
  /**
245
285
  * @param {WebSocketMessageData} data
246
286
  */
@@ -73,7 +73,7 @@ class CucumberPlugin extends CiPlugin {
73
73
  isEarlyFlakeDetectionFaulty,
74
74
  isTestManagementTestsEnabled,
75
75
  isParallel,
76
- isExpectedEmptySession,
76
+ testSessionEmptyReason,
77
77
  error,
78
78
  onDone,
79
79
  }) => {
@@ -118,12 +118,11 @@ class CucumberPlugin extends CiPlugin {
118
118
 
119
119
  this.testSessionSpan.setTag(TEST_STATUS, status)
120
120
  this.testModuleSpan.setTag(TEST_STATUS, status)
121
- if (isExpectedEmptySession) {
121
+ if (testSessionEmptyReason) {
122
122
  setExpectedEmptyTestSessionTags(
123
123
  this.testSessionSpan,
124
124
  this.testModuleSpan,
125
- 'No scenarios were executed',
126
- 'zero_tests'
125
+ testSessionEmptyReason
127
126
  )
128
127
  }
129
128
  if (error) {
@@ -242,7 +242,7 @@ function getSessionStatus (summary, testSuiteStatuses) {
242
242
  if (summary.status === 'failed' || summary.failures > 0 || summary.totalFailed > 0) {
243
243
  return 'fail'
244
244
  }
245
- if (summary.totalSkipped !== undefined && summary.totalSkipped === summary.totalTests) {
245
+ if ((summary.totalSkipped ?? 0) + (summary.totalPending ?? 0) === summary.totalTests) {
246
246
  return 'skip'
247
247
  }
248
248
  return 'pass'
@@ -381,7 +381,7 @@ function getSuiteStatus (suiteStats) {
381
381
  return 'fail'
382
382
  }
383
383
  if (suiteStats.tests !== undefined &&
384
- (suiteStats.tests === suiteStats.pending || suiteStats.tests === suiteStats.skipped)) {
384
+ suiteStats.tests === (suiteStats.pending ?? 0) + (suiteStats.skipped ?? 0)) {
385
385
  return 'skip'
386
386
  }
387
387
  return 'pass'
@@ -451,6 +451,8 @@ function getFinalStatus ({
451
451
  }
452
452
 
453
453
  class CypressPlugin {
454
+ #pendingTestSuiteSpans = new Map()
455
+
454
456
  _isInit = false
455
457
  testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
456
458
 
@@ -548,6 +550,7 @@ class CypressPlugin {
548
550
  *
549
551
  */
550
552
  resetRunState () {
553
+ this.#pendingTestSuiteSpans.clear()
551
554
  this._isInit = false
552
555
  this.finishedTestsByFile = {}
553
556
  this.hasTestsReported = false
@@ -1048,7 +1051,7 @@ class CypressPlugin {
1048
1051
  }
1049
1052
  }
1050
1053
 
1051
- return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_suite`, {
1054
+ const testSuiteSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_suite`, {
1052
1055
  childOf: this.testModuleSpan,
1053
1056
  tags: {
1054
1057
  [COMPONENT]: TEST_FRAMEWORK_NAME,
@@ -1057,9 +1060,13 @@ class CypressPlugin {
1057
1060
  },
1058
1061
  integrationName: TEST_FRAMEWORK_NAME,
1059
1062
  })
1063
+ this.#pendingTestSuiteSpans.set(testSuite, testSuiteSpan)
1064
+ return testSuiteSpan
1060
1065
  }
1061
1066
 
1062
- getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined }) {
1067
+ getTestSpan ({
1068
+ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile, isDisabled, isQuarantined, startTime,
1069
+ }) {
1063
1070
  const testSuiteTags = {
1064
1071
  [TEST_MODULE]: TEST_FRAMEWORK_NAME,
1065
1072
  }
@@ -1116,6 +1123,7 @@ class CypressPlugin {
1116
1123
 
1117
1124
  return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
1118
1125
  childOf,
1126
+ startTime,
1119
1127
  tags: {
1120
1128
  [COMPONENT]: TEST_FRAMEWORK_NAME,
1121
1129
  [ORIGIN_KEY]: CI_APP_ORIGIN,
@@ -1348,6 +1356,20 @@ class CypressPlugin {
1348
1356
  }
1349
1357
 
1350
1358
  afterRun (suiteStats, error) {
1359
+ const pendingFinalizations = []
1360
+ // Late manual initialization cannot intercept a later replacement of after:spec.
1361
+ // Run results let us finalize the suites whose browser hooks already created spans.
1362
+ if (this._isInit && suiteStats?.runs) {
1363
+ for (const result of suiteStats.runs) {
1364
+ const testSuiteSpan = this.#pendingTestSuiteSpans.get(result.spec?.relative)
1365
+ if (!testSuiteSpan) continue
1366
+ this.testSuiteSpan = testSuiteSpan
1367
+ const endedAt = Date.parse(result.stats?.endedAt)
1368
+ const finalization = this.afterSpec(result.spec, result, undefined,
1369
+ Number.isFinite(endedAt) ? endedAt : undefined)
1370
+ if (finalization) pendingFinalizations.push(finalization)
1371
+ }
1372
+ }
1351
1373
  const hasPendingVideoSpans = this.pendingVideoUploads.length > 0
1352
1374
  const videoUploadsPromise = this.uploadPendingTestSuiteVideos()
1353
1375
  if (!this._isInit) {
@@ -1358,7 +1380,11 @@ class CypressPlugin {
1358
1380
  }
1359
1381
  return
1360
1382
  }
1361
- const finalizationPromise = this.#finalizeRun(suiteStats, error, hasPendingVideoSpans)
1383
+ // Recovered screenshot-only tests must finish before the run closes any remaining trace spans.
1384
+ const finalizeRun = () => this.#finalizeRun(suiteStats, error, hasPendingVideoSpans)
1385
+ const finalizationPromise = pendingFinalizations.length > 0
1386
+ ? Promise.all(pendingFinalizations).then(finalizeRun)
1387
+ : finalizeRun()
1362
1388
  if (videoUploadsPromise) {
1363
1389
  return Promise.all([videoUploadsPromise, finalizationPromise])
1364
1390
  .then(() => this.#flushExporter(false))
@@ -1384,12 +1410,11 @@ class CypressPlugin {
1384
1410
 
1385
1411
  this.testModuleSpan.setTag(TEST_STATUS, testStatus)
1386
1412
  this.testSessionSpan.setTag(TEST_STATUS, testStatus)
1387
- if (testStatus !== 'fail' && hasNoTests) {
1413
+ if (testStatus !== 'fail' && (hasNoTests || testStatus === 'skip')) {
1388
1414
  setExpectedEmptyTestSessionTags(
1389
1415
  this.testSessionSpan,
1390
1416
  this.testModuleSpan,
1391
- 'No tests were executed',
1392
- 'zero_tests'
1417
+ hasNoTests ? 'zero_tests' : 'all_tests_skipped'
1393
1418
  )
1394
1419
  }
1395
1420
  if (error) {
@@ -1504,10 +1529,10 @@ class CypressPlugin {
1504
1529
  }
1505
1530
  }
1506
1531
 
1507
- afterSpec (spec, results, error) {
1532
+ afterSpec (spec, results, error, finishTime) {
1508
1533
  const { tests, stats, screenshots, video, error: resultError } = results || {}
1509
1534
  const cypressTests = tests || []
1510
- if (cypressTests.length > 0) this.hasTestsReported = true
1535
+ if (cypressTests.length > 0 || stats?.tests > 0) this.hasTestsReported = true
1511
1536
  const specScreenshots = screenshots || []
1512
1537
  const finishedTests = this.finishedTestsByFile[spec.relative] || []
1513
1538
  const screenshotUploadPromises = []
@@ -1540,7 +1565,10 @@ class CypressPlugin {
1540
1565
  ? getTestSuitePath(spec.absolute, this.repositoryRoot)
1541
1566
  : spec.relative
1542
1567
 
1543
- const skippedTestSpan = this.getTestSpan({ testName: cypressTestName, testSuite: spec.relative, testSourceFile })
1568
+ // Recovered tests that never ran have no execution time; keep them at the recorded suite end.
1569
+ const skippedTestSpan = this.getTestSpan({
1570
+ testName: cypressTestName, testSuite: spec.relative, testSourceFile, startTime: finishTime,
1571
+ })
1544
1572
  skippedTestSpan.setTag(TEST_FINAL_STATUS, 'skip')
1545
1573
 
1546
1574
  skippedTestSpan.setTag(TEST_STATUS, 'skip')
@@ -1578,7 +1606,7 @@ class CypressPlugin {
1578
1606
  })
1579
1607
  }
1580
1608
 
1581
- testSpanFinishes.push({ testSpan: skippedTestSpan, finishTime: this._now() })
1609
+ testSpanFinishes.push({ testSpan: skippedTestSpan, finishTime: finishTime ?? this._now() })
1582
1610
  }
1583
1611
 
1584
1612
  // Make sure that reported test statuses are the same as Cypress reports.
@@ -1749,7 +1777,7 @@ class CypressPlugin {
1749
1777
  }
1750
1778
  }
1751
1779
 
1752
- const testSuiteFinishTime = this._now()
1780
+ const testSuiteFinishTime = finishTime ?? this._now()
1753
1781
  const suiteError = error || resultError || latestError
1754
1782
  const suiteStatus = suiteError ||
1755
1783
  cypressTests.some(test => CYPRESS_STATUS_TO_TEST_STATUS[test.state] === 'fail')
@@ -1771,6 +1799,7 @@ class CypressPlugin {
1771
1799
  testSuiteSpan.setTag(TEST_STATUS, suiteStatus)
1772
1800
  if (suiteError) testSuiteSpan.setTag('error', suiteError)
1773
1801
  this.testSuiteSpan = null
1802
+ this.#pendingTestSuiteSpans.delete(spec.relative)
1774
1803
  }
1775
1804
 
1776
1805
  let testSpansPromise
@@ -2034,7 +2063,8 @@ class CypressPlugin {
2034
2063
  rumTestExecutionIdCookieName: RUM_TEST_EXECUTION_ID_COOKIE_NAME,
2035
2064
  }
2036
2065
 
2037
- this.testSuiteSpan ||= this.getTestSuiteSpan({ testSuite, testSuiteAbsolutePath })
2066
+ this.testSuiteSpan = this.#pendingTestSuiteSpans.get(testSuite) ||
2067
+ this.getTestSuiteSpan({ testSuite, testSuiteAbsolutePath })
2038
2068
  return suitePayload
2039
2069
  },
2040
2070
  'dd:beforeEach': (test) => {
@@ -26,6 +26,7 @@ class CypressPlugin extends Plugin {
26
26
  userAfterRunHandlers,
27
27
  userAfterScreenshotHandlers,
28
28
  cleanupWrapper,
29
+ registerBeforeRun,
29
30
  } = payload
30
31
 
31
32
  const registerAfterRunWithCleanup = (afterRunHandler) => {
@@ -79,6 +80,7 @@ class CypressPlugin extends Plugin {
79
80
  }
80
81
 
81
82
  if (cypressPlugin._isInit) {
83
+ registerBeforeRun()
82
84
  // Already initialized by manual plugin call — just chain user handlers.
83
85
  // Pass the plugin's afterScreenshot so chaining a user handler doesn't drop the upload
84
86
  // (the chained registration replaces the one plugin.js set, so it must include it).
@@ -94,7 +96,7 @@ class CypressPlugin extends Plugin {
94
96
  return
95
97
  }
96
98
 
97
- on('before:run', cypressPlugin.beforeRun.bind(cypressPlugin))
99
+ registerBeforeRun(cypressPlugin.beforeRun.bind(cypressPlugin))
98
100
  registerAfterScreenshot(datadogAfterScreenshotHandler)
99
101
 
100
102
  on('after:spec', (spec, results) => {
@@ -117,19 +117,18 @@ class JestPlugin extends CiPlugin {
117
117
  isEarlyFlakeDetectionEnabled,
118
118
  isEarlyFlakeDetectionFaulty,
119
119
  isTestManagementTestsEnabled,
120
- isExpectedEmptySession,
120
+ testSessionEmptyReason,
121
121
  onDone,
122
122
  }) => {
123
123
  const finishSession = () => {
124
124
  this.testSessionSpan.setTag(TEST_STATUS, status)
125
125
  this.testModuleSpan.setTag(TEST_STATUS, status)
126
126
 
127
- if (isExpectedEmptySession) {
127
+ if (testSessionEmptyReason) {
128
128
  setExpectedEmptyTestSessionTags(
129
129
  this.testSessionSpan,
130
130
  this.testModuleSpan,
131
- 'No tests were found',
132
- 'zero_tests'
131
+ testSessionEmptyReason
133
132
  )
134
133
  }
135
134
 
@@ -885,7 +885,7 @@ class MochaPlugin extends CiPlugin {
885
885
  isTestManagementEnabled,
886
886
  isParallel,
887
887
  isFrameworkError,
888
- isExpectedEmptySession,
888
+ testSessionEmptyReason,
889
889
  onDone,
890
890
  }) => {
891
891
  this._exportPendingWorkerTraces()
@@ -898,12 +898,11 @@ class MochaPlugin extends CiPlugin {
898
898
  this.testSessionSpan.setTag(TEST_STATUS, status)
899
899
  this.testModuleSpan.setTag(TEST_STATUS, status)
900
900
 
901
- if (isExpectedEmptySession) {
901
+ if (testSessionEmptyReason) {
902
902
  setExpectedEmptyTestSessionTags(
903
903
  this.testSessionSpan,
904
904
  this.testModuleSpan,
905
- 'No tests were executed',
906
- 'zero_tests'
905
+ testSessionEmptyReason
907
906
  )
908
907
  }
909
908
 
@@ -70,8 +70,6 @@ const log = require('../../dd-trace/src/log')
70
70
 
71
71
  const PLAYWRIGHT_FAILURE_SCREENSHOT_RE = /^test-failed-\d+\.png$/
72
72
  const PLAYWRIGHT_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm'])
73
- const EMPTY_SHARD_SKIP_REASON = 'No tests were assigned to this shard'
74
- const EMPTY_SHARD_REASON = 'zero_test_shard'
75
73
  const RETRY_TEST_ID = '_dd.playwright.retry_test_id'
76
74
  const DEFER_FINAL_STATUS = '_dd.playwright.defer_final_status'
77
75
  const noop = () => {}
@@ -186,7 +184,7 @@ class PlaywrightPlugin extends CiPlugin {
186
184
  isEarlyFlakeDetectionEnabled,
187
185
  isEarlyFlakeDetectionFaulty,
188
186
  isTestManagementTestsEnabled,
189
- isExpectedEmptyShard,
187
+ testSessionEmptyReason,
190
188
  error,
191
189
  onDone,
192
190
  }) => {
@@ -212,12 +210,11 @@ class PlaywrightPlugin extends CiPlugin {
212
210
  this.testModuleSpan.setTag(TEST_STATUS, status)
213
211
  this.testSessionSpan.setTag(TEST_STATUS, status)
214
212
 
215
- if (isExpectedEmptyShard) {
213
+ if (testSessionEmptyReason) {
216
214
  setExpectedEmptyTestSessionTags(
217
215
  this.testSessionSpan,
218
216
  this.testModuleSpan,
219
- EMPTY_SHARD_SKIP_REASON,
220
- EMPTY_SHARD_REASON
217
+ testSessionEmptyReason
221
218
  )
222
219
  }
223
220
 
@@ -526,6 +523,8 @@ class PlaywrightPlugin extends CiPlugin {
526
523
  this.addSub('ci:playwright:test:finish', ({
527
524
  span,
528
525
  testStatus,
526
+ isExpectedFailure,
527
+ hasNonRetriableError,
529
528
  steps,
530
529
  error,
531
530
  extraTags,
@@ -565,6 +564,12 @@ class PlaywrightPlugin extends CiPlugin {
565
564
  if (extraTags) {
566
565
  span.addTags(extraTags)
567
566
  }
567
+ if (isExpectedFailure) {
568
+ span.setTag('test.result', 'xfail')
569
+ }
570
+ if (hasNonRetriableError === true) {
571
+ span.setTag('test.playwright.has_non_retriable_error', 'true')
572
+ }
568
573
  if (isNew) {
569
574
  span.setTag(TEST_IS_NEW, 'true')
570
575
  if (isEfdRetry) {
@@ -580,7 +580,7 @@ class VitestPlugin extends CiPlugin {
580
580
  requestErrorTags,
581
581
  vitestPool,
582
582
  isVitestNoWorkerInitActive,
583
- isExpectedEmptySession,
583
+ testSessionEmptyReason,
584
584
  onDone,
585
585
  }) => {
586
586
  for (const [tag, value] of Object.entries(requestErrorTags)) {
@@ -589,12 +589,11 @@ class VitestPlugin extends CiPlugin {
589
589
  }
590
590
  this.testSessionSpan.setTag(TEST_STATUS, status)
591
591
  this.testModuleSpan.setTag(TEST_STATUS, status)
592
- if (isExpectedEmptySession) {
592
+ if (testSessionEmptyReason) {
593
593
  setExpectedEmptyTestSessionTags(
594
594
  this.testSessionSpan,
595
595
  this.testModuleSpan,
596
- 'No tests were executed',
597
- 'zero_tests'
596
+ testSessionEmptyReason
598
597
  )
599
598
  }
600
599
  if (error) {
@@ -21,7 +21,7 @@ class WSProducerPlugin extends TracingPlugin {
21
21
 
22
22
  bindStart (ctx) {
23
23
  const { byteLength, socket, binary } = ctx
24
- if (!socket.spanContext) return
24
+ if (!socket?.spanContext) return
25
25
 
26
26
  const spanTags = socket.spanTags
27
27
  const path = getSegment(spanTags['resource.name'], ' ', 1)
@@ -53,8 +53,15 @@ function copyProperties (original, wrapped) {
53
53
  const descriptor = /** @type {Descriptor} */ (Object.getOwnPropertyDescriptor(original, key))
54
54
  if (descriptor.writable && descriptor.enumerable && descriptor.configurable) {
55
55
  wrapped[key] = original[key]
56
- } else if (descriptor.writable || descriptor.configurable || !Object.hasOwn(wrapped, key)) {
57
- Object.defineProperty(wrapped, key, descriptor)
56
+ } else {
57
+ try {
58
+ Object.defineProperty(wrapped, key, descriptor)
59
+ } catch (error) {
60
+ const wrappedDescriptor = Object.getOwnPropertyDescriptor(wrapped, key)
61
+ if (wrappedDescriptor?.configurable !== false || wrappedDescriptor.writable) {
62
+ throw error
63
+ }
64
+ }
58
65
  }
59
66
  }
60
67
  }
@@ -320,19 +327,16 @@ function assertMethod (target, name, method) {
320
327
  }
321
328
 
322
329
  /**
323
- * Asserts that a target is not a class constructor.
330
+ * Asserts that a target is not an identifiable class constructor.
324
331
  *
325
332
  * @param {Function} target - The target function.
326
- * @throws {Error} If the target is a class constructor.
333
+ * @throws {TypeError} If the target is an identifiable class constructor.
327
334
  */
328
335
  function assertNotClass (target) {
329
- // Class constructors have a non-writable `prototype` property; functions have a
330
- // writable one and arrows / async / method-shorthand have none at all. The
331
- // `'prototype' in target` gate skips the descriptor lookup for the no-prototype
332
- // shapes; the `in` operator is cheaper than reading `target.prototype` since
333
- // it returns a boolean instead of materialising the prototype reference.
336
+ // Frozen functions also have a non-writable `prototype`; inspect their source only then.
334
337
  if ('prototype' in target &&
335
- Object.getOwnPropertyDescriptor(target, 'prototype').writable === false) {
338
+ Object.getOwnPropertyDescriptor(target, 'prototype').writable === false &&
339
+ (!Object.isFrozen(target) || Function.prototype.toString.call(target).startsWith('class'))) {
336
340
  throw new TypeError('Target is a native class constructor and cannot be wrapped.')
337
341
  }
338
342
  }
@@ -6,6 +6,7 @@ const path = require('node:path')
6
6
 
7
7
  const satisfies = require('../../vendor/dist/semifies')
8
8
  const hooks = require('../datadog-instrumentations/src/helpers/hooks')
9
+ const { getRewriteTargetNames } = require('../datadog-instrumentations/src/helpers/rewriter/targets')
9
10
  const { SYNTHETIC_EXTENSION } = require('./src/constants')
10
11
 
11
12
  const BUILTIN_MODULES = new Set(Module.builtinModules)
@@ -158,7 +159,9 @@ function hasDatadogLoader (value) {
158
159
  */
159
160
  function createPackagePathPattern () {
160
161
  const names = []
161
- for (const name of Object.keys(hooks)) {
162
+ const targetPackages = new Set(Object.keys(hooks))
163
+ for (const name of getRewriteTargetNames()) targetPackages.add(name)
164
+ for (const name of targetPackages) {
162
165
  if (!name.startsWith('.') && !BUILTIN_MODULES.has(name)) names.push(escapeRegExp(name))
163
166
  }
164
167
  names.sort()
@@ -11,7 +11,10 @@ const hooks = require('../../datadog-instrumentations/src/helpers/hooks')
11
11
  const instrumentations = require('../../datadog-instrumentations/src/helpers/instrumentations')
12
12
  const { filename, matchesInstrumentation } = require('../../datadog-instrumentations/src/helpers/instrumentation-utils')
13
13
  const { createBundlerRewriter } = require('../../datadog-instrumentations/src/helpers/rewriter')
14
- const { getRewriteTarget } = require('../../datadog-instrumentations/src/helpers/rewriter/targets')
14
+ const {
15
+ getRewriteTarget,
16
+ getRewriteTargetNames,
17
+ } = require('../../datadog-instrumentations/src/helpers/rewriter/targets')
15
18
  const { isESMFile } = require('../../datadog-esbuild/src/utils')
16
19
  const { SYNTHETIC_EXTENSION } = require('./constants')
17
20
 
@@ -21,6 +24,7 @@ const MODULE_BINDING = 2
21
24
  const REQUIRE_BINDING = 4
22
25
  const COMMONJS_BINDINGS = MODULE_BINDING | REQUIRE_BINDING
23
26
  const targetPackages = new Set(Object.keys(hooks))
27
+ for (const name of getRewriteTargetNames()) targetPackages.add(name)
24
28
  const entrypoints = new Map()
25
29
  const loadedHooks = new Set()
26
30
  const packageCache = new Map()
@@ -98,7 +102,7 @@ module.exports = function loader (source, inputSourceMap) {
98
102
  }
99
103
 
100
104
  if (publications.length > 0) code = appendCommonJsPublications(code, publications, dcModule)
101
- if (rewritten && publications.length === 0) {
105
+ if (rewritten && publications.length === 0 && (rewriteTarget.activate || hooks[rewriteTarget.moduleName])) {
102
106
  if (!esm && hasUnsafeCommonJsBindings(code)) {
103
107
  this.emitWarning?.(new Error(`Skipped CommonJS activation for unsafe wrapper bindings in ${resourcePath}`))
104
108
  this.callback(undefined, source, inputSourceMap)
@@ -17,21 +17,22 @@ module.exports = {
17
17
  * Fetches agent information from the /info endpoint
18
18
  * @param {URL} url - The agent URL
19
19
  * @param {Function} callback - Callback function with signature (err, agentInfo)
20
- * @param {{ deadline?: number, signal?: AbortSignal }} [options] - Request finalization options
20
+ * @param {{ deadline?: number, signal?: AbortSignal, path?: string, retry?: boolean }} [options]
21
+ * Request finalization and routing options
21
22
  * @param {Function} [makeRequest] - Request implementation
22
23
  */
23
24
  function fetchAgentInfo (url, callback, options = {}, makeRequest = request) {
24
- const urlKey = url.href
25
+ const urlKey = JSON.stringify([url.href, options.path ?? '/info'])
25
26
 
26
27
  if (cachedUrl !== null && cachedUrl !== urlKey) {
27
- // Clear cache if URL changes
28
+ // Different consumers may query different paths on the same Agent URL.
28
29
  clearCache()
29
30
  } else if (cachedData !== null && (Date.now() - cachedTimestamp) < CACHE_TTL_MS) {
30
31
  // Return cached result if still valid
31
32
  return process.nextTick(callback, null, cachedData)
32
33
  }
33
34
 
34
- options.path = '/info'
35
+ options.path ??= '/info'
35
36
  options.url = url
36
37
  makeRequest('', options, (err, res) => {
37
38
  if (err) {