dd-trace 6.10.0 → 6.11.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 (137) hide show
  1. package/README.md +0 -17
  2. package/index.d.ts +108 -3
  3. package/initialize.mjs +28 -17
  4. package/openfeature.js +2 -2
  5. package/package.json +11 -8
  6. package/packages/datadog-esbuild/index.js +0 -34
  7. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  9. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  10. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  11. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  12. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  13. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  14. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  15. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  17. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  18. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
  19. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  20. package/packages/datadog-instrumentations/src/jest.js +25 -17
  21. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  22. package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +130 -17
  24. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  25. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  26. package/packages/datadog-instrumentations/src/router.js +2 -0
  27. package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  30. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
  31. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  32. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  33. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  34. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  35. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  36. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
  38. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  39. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  40. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  41. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  42. package/packages/datadog-plugin-electron/src/index.js +2 -0
  43. package/packages/datadog-plugin-fs/src/index.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +6 -0
  45. package/packages/datadog-plugin-mocha/src/index.js +56 -1
  46. package/packages/datadog-plugin-nats/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  48. package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  50. package/packages/datadog-plugin-vitest/src/index.js +6 -0
  51. package/packages/datadog-webpack/index.js +0 -11
  52. package/packages/dd-trace/index.js +0 -1
  53. package/packages/dd-trace/src/agent/info.js +6 -5
  54. package/packages/dd-trace/src/aiguard/client.js +100 -1
  55. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  56. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  57. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  58. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  59. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  60. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  61. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  62. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  63. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  64. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  66. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  69. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  70. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  71. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  72. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  73. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  74. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  75. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  76. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  77. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  78. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  79. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  80. package/packages/dd-trace/src/config/index.js +13 -2
  81. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  82. package/packages/dd-trace/src/config/supported-configurations.json +14 -0
  83. package/packages/dd-trace/src/datastreams/index.js +2 -0
  84. package/packages/dd-trace/src/datastreams/manager.js +9 -2
  85. package/packages/dd-trace/src/datastreams/processor.js +15 -13
  86. package/packages/dd-trace/src/datastreams/size.js +5 -0
  87. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  88. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  89. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  90. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  91. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  92. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  93. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  94. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  95. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  96. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  97. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  98. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  99. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  100. package/packages/dd-trace/src/noop/proxy.js +3 -4
  101. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  102. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  103. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  104. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  105. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  106. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  107. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  108. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  109. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  110. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  111. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  112. package/packages/dd-trace/src/plugin_manager.js +3 -3
  113. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  114. package/packages/dd-trace/src/plugins/index.js +1 -0
  115. package/packages/dd-trace/src/plugins/util/jest.js +4 -3
  116. package/packages/dd-trace/src/plugins/util/test.js +9 -9
  117. package/packages/dd-trace/src/plugins/util/web.js +3 -0
  118. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  119. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  120. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  121. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  122. package/packages/dd-trace/src/proxy.js +38 -29
  123. package/packages/dd-trace/src/ritm.js +9 -2
  124. package/packages/dd-trace/src/serverless.js +37 -0
  125. package/packages/dd-trace/src/span_format.js +1 -0
  126. package/packages/dd-trace/src/span_stats.js +30 -3
  127. package/packages/dd-trace/src/storage-channels.js +86 -0
  128. package/packages/dd-trace/src/tracer.js +9 -2
  129. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  130. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  131. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  133. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  134. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  135. package/packages/dd-trace/src/feature-registry.js +0 -29
  136. package/packages/dd-trace/src/openfeature/register.js +0 -35
  137. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -35,9 +35,13 @@ const testSuiteErrorCh = channel('ci:mocha:test-suite:error')
35
35
  /** @typedef {{ length: number, [index: number]: unknown } & Iterable<unknown>} ArgumentsLike */
36
36
 
37
37
  const testToContext = new WeakMap()
38
+ const originalEfdFns = new WeakMap()
38
39
  const originalFns = new WeakMap()
40
+ const originalPendingByTest = new WeakMap()
41
+ const originalRetriesByTest = new WeakMap()
39
42
  const testToStartLine = new WeakMap()
40
43
  const testFileToSuiteCtx = new Map()
44
+ const datadogRetryOriginals = new WeakMap()
41
45
  const wrappedFunctions = new WeakSet()
42
46
  const newTests = {}
43
47
  const efdTests = {}
@@ -169,6 +173,7 @@ function wrapOriginalEfdTest (test, retryPolicy) {
169
173
  }
170
174
  test._ddEfdDurationWrapped = true
171
175
  const originalFn = test.fn
176
+ originalEfdFns.set(test, originalFn)
172
177
  test.fn = shimmer.wrapFunction(originalFn, originalFn => function () {
173
178
  const start = performance.now()
174
179
  const recordDuration = () => {
@@ -265,6 +270,7 @@ function isEarlyFlakeDetectionTest (test, config) {
265
270
  function retryTest (test, numRetries, tags, retryPolicy) {
266
271
  const suite = test.parent
267
272
  const isEfdRetry = tags.includes('_ddIsEfdRetry')
273
+ if (!originalRetriesByTest.has(test)) originalRetriesByTest.set(test, test.retries())
268
274
  disableMochaRetries(test)
269
275
  if (isEfdRetry) {
270
276
  wrapOriginalEfdTest(test, retryPolicy)
@@ -272,6 +278,7 @@ function retryTest (test, numRetries, tags, retryPolicy) {
272
278
  for (let retryIndex = 0; retryIndex < numRetries; retryIndex++) {
273
279
  const clonedTest = test.clone()
274
280
  disableMochaRetries(clonedTest)
281
+ datadogRetryOriginals.set(clonedTest, test)
275
282
  suite.addTest(clonedTest)
276
283
  if (isEfdRetry) {
277
284
  clonedTest._ddEfdRetryIndex = retryIndex + 1
@@ -295,6 +302,91 @@ function retryTest (test, numRetries, tags, retryPolicy) {
295
302
  }
296
303
  }
297
304
 
305
+ /**
306
+ * Restores a runnable function wrapped with its Test Optimization context.
307
+ *
308
+ * @param {import('mocha').Runnable} runnable
309
+ * @returns {void}
310
+ */
311
+ function restoreRunnableFunction (runnable) {
312
+ const wrappedFunction = runnable.fn
313
+ if (!wrappedFunctions.has(wrappedFunction)) return
314
+
315
+ runnable.fn = originalFns.get(wrappedFunction)
316
+ }
317
+
318
+ /**
319
+ * Restores a test function wrapped to measure its EFD duration.
320
+ *
321
+ * @param {import('mocha').Test} test
322
+ * @returns {void}
323
+ */
324
+ function restoreEfdTestFunction (test) {
325
+ if (!originalEfdFns.has(test)) return
326
+
327
+ test.fn = originalEfdFns.get(test)
328
+ originalEfdFns.delete(test)
329
+ delete test._ddEfdDurationWrapped
330
+ }
331
+
332
+ /**
333
+ * Clears state that belongs to one Mocha runner execution and removes retry clones from prior executions.
334
+ *
335
+ * @param {import('mocha').Suite} rootSuite
336
+ * @returns {void}
337
+ */
338
+ function resetRunState (rootSuite) {
339
+ for (const key of Object.keys(newTests)) delete newTests[key]
340
+ for (const key of Object.keys(efdTests)) delete efdTests[key]
341
+ newTestsWithDynamicNames.clear()
342
+ testsAttemptToFix.clear()
343
+ testsQuarantined.clear()
344
+ testsStatuses.clear()
345
+ efdRetryCountByTestFullName.clear()
346
+ efdSlowAbortedTests.clear()
347
+ attemptToFixExecutions.clear()
348
+ loggedAttemptToFixTests.clear()
349
+
350
+ const suites = [rootSuite]
351
+ while (suites.length) {
352
+ const suite = suites.pop()
353
+ const originalTests = []
354
+ const retainedTests = new Set()
355
+ for (const runnable of suite.tests) {
356
+ const test = datadogRetryOriginals.get(runnable) || runnable
357
+ if (retainedTests.has(test)) continue
358
+ retainedTests.add(test)
359
+
360
+ restoreRunnableFunction(test)
361
+ restoreEfdTestFunction(test)
362
+ if (originalPendingByTest.has(test)) {
363
+ test.pending = originalPendingByTest.get(test)
364
+ originalPendingByTest.delete(test)
365
+ }
366
+ if (originalRetriesByTest.has(test)) {
367
+ test.retries(originalRetriesByTest.get(test))
368
+ originalRetriesByTest.delete(test)
369
+ }
370
+
371
+ delete test._ddIsAttemptToFix
372
+ delete test._ddIsDisabled
373
+ delete test._ddIsQuarantined
374
+ delete test._ddIsModified
375
+ delete test._ddIsNew
376
+ delete test._ddShouldSkipEfdRetry
377
+ delete test._ddTestFinishStarted
378
+ delete test._ddTestFinishPublished
379
+ delete test._ddIsFinalAttempt
380
+ delete test._ddHookFailed
381
+ delete test._ddReporterStartFailed
382
+ delete test._ddReporterTerminalFailed
383
+ originalTests.push(test)
384
+ }
385
+ suite.tests = originalTests
386
+ suites.push(...suite.suites)
387
+ }
388
+ }
389
+
298
390
  function getSuitesByTestFile (root) {
299
391
  const suitesByTestFile = {}
300
392
  function getSuites (suite) {
@@ -371,6 +463,19 @@ function getTestContext (test) {
371
463
  return testToContext.get(key)
372
464
  }
373
465
 
466
+ /**
467
+ * Claims publication of a test finish event across Mocha's terminal event paths.
468
+ *
469
+ * @param {object} test
470
+ * @returns {boolean}
471
+ */
472
+ function startTestFinish (test) {
473
+ if (test._ddTestFinishStarted || test._ddTestFinishPublished) return false
474
+
475
+ test._ddTestFinishStarted = true
476
+ return true
477
+ }
478
+
374
479
  /**
375
480
  * Copies Test Management metadata from Mocha's original runnable to its native retry clone.
376
481
  * @param {{
@@ -413,11 +518,7 @@ function runnableWrapper (RunnablePackage, libraryConfig) {
413
518
  const test = isTestHook ? this.ctx.currentTest : this
414
519
 
415
520
  // we restore the original user defined function
416
- if (wrappedFunctions.has(this.fn)) {
417
- const originalFn = originalFns.get(this.fn)
418
- this.fn = originalFn
419
- wrappedFunctions.delete(this.fn)
420
- }
521
+ restoreRunnableFunction(this)
421
522
 
422
523
  if (isDatadogManagedRetryTest(test, libraryConfig)) {
423
524
  disableMochaRetries(this)
@@ -460,11 +561,7 @@ function getOnTestHandler (isMain) {
460
561
 
461
562
  // This may be a retry. If this is the case, `test.fn` is already wrapped,
462
563
  // so we need to restore it.
463
- if (wrappedFunctions.has(test.fn)) {
464
- const originalFn = originalFns.get(test.fn)
465
- test.fn = originalFn
466
- wrappedFunctions.delete(test.fn)
467
- }
564
+ restoreRunnableFunction(test)
468
565
 
469
566
  inheritDatadogPropertiesFromRetriedTest(test)
470
567
 
@@ -527,6 +624,9 @@ function getOnTestHandler (isMain) {
527
624
  }
528
625
 
529
626
  if (!isAttemptToFix && isDisabled) {
627
+ if (!originalPendingByTest.has(test)) {
628
+ originalPendingByTest.set(test, test.pending)
629
+ }
530
630
  test.pending = true
531
631
  }
532
632
 
@@ -701,9 +801,16 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
701
801
  if (test._ddShouldSkipEfdRetry) {
702
802
  return
703
803
  }
804
+ const shouldWaitForHitProbe = test._retriedTest?._ddShouldWaitForHitProbe
704
805
  const ctx = getTestContext(test)
705
806
  const status = getTestStatus(test)
706
- const shouldFinishTest = ctx && (!getAfterEachHooks(test).length || (test._ddIsDisabled && !test._ddIsAttemptToFix))
807
+ const shouldFinishTest = ctx && (
808
+ test._ddReporterStartFailed ||
809
+ test._ddReporterTerminalFailed ||
810
+ !getAfterEachHooks(test).length ||
811
+ (test._ddIsDisabled && !test._ddIsAttemptToFix)
812
+ )
813
+ if (shouldFinishTest && !startTestFinish(test)) return
707
814
  let testFinishInfo
708
815
  let isFinalAttempt = false
709
816
 
@@ -729,11 +836,12 @@ function getOnTestEndHandler (config, finalAttemptHandlers) {
729
836
  finalAttemptHandlers?.onStart?.(test)
730
837
  }
731
838
 
732
- if (test._retriedTest?._ddShouldWaitForHitProbe) {
839
+ if (shouldWaitForHitProbe) {
733
840
  await waitForHitProbe()
734
841
  }
735
842
 
736
843
  if (shouldFinishTest) {
844
+ test._ddTestFinishPublished = true
737
845
  testFinishCh.publish({
738
846
  status,
739
847
  hasBeenRetried: isMochaRetry(test),
@@ -760,10 +868,11 @@ function getOnHookEndHandler (config, finalAttemptHandlers) {
760
868
  const ctx = getTestContext(test)
761
869
  // Disabled tests are already finished in getOnTestEndHandler,
762
870
  // skip to avoid double-publishing
763
- if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix)) {
871
+ if (ctx && (!test._ddIsDisabled || test._ddIsAttemptToFix) && startTestFinish(test)) {
764
872
  const testFinishInfo = getTestFinishInfo(test, status, config, ctx.err || test.err)
765
873
  const isFinalAttempt = testFinishInfo.finalStatus !== undefined
766
874
  const publishTestFinish = () => {
875
+ test._ddTestFinishPublished = true
767
876
  testFinishCh.publish({
768
877
  status,
769
878
  hasBeenRetried: isMochaRetry(test),
@@ -887,11 +996,13 @@ function getOnFailHandler (isMain, config) {
887
996
  testContext = getTestContext(test)
888
997
  }
889
998
  if (testContext) {
890
- if (isHook) {
891
- err.message = `${testOrHook.fullTitle()}: ${err.message}`
892
- testContext.err = err
999
+ if (isHook && startTestFinish(test)) {
1000
+ const hookError = new Error(`${testOrHook.fullTitle()}: ${err.message}`, { cause: err })
1001
+ hookError.name = err.name
1002
+ hookError.stack = err.stack
1003
+ testContext.err = hookError
893
1004
  errorCh.runStores(testContext, () => {})
894
- const testFinishInfo = getTestFinishInfo(test, 'fail', config, err)
1005
+ const testFinishInfo = getTestFinishInfo(test, 'fail', config, hookError)
895
1006
  // ATR never retries hook failures: this.retries(N) is set in runnableWrapper
896
1007
  // which only runs when the test function executes — hooks bypass that path,
897
1008
  // so _retries stays at -1 and getIsLastRetry returns false, leaving finalStatus
@@ -906,6 +1017,7 @@ function getOnFailHandler (isMain, config) {
906
1017
  // test.state is never set to 'failed' for hook failures (Mocha marks the hook,
907
1018
  // not the test). Flag it so finishRootSuiteForFile can compute the correct status.
908
1019
  test._ddHookFailed = true
1020
+ test._ddTestFinishPublished = true
909
1021
  testFinishCh.publish({
910
1022
  status: 'fail',
911
1023
  hasBeenRetried: isMochaRetry(test),
@@ -1088,6 +1200,7 @@ module.exports = {
1088
1200
  getOnPendingHandler,
1089
1201
  testFileToSuiteCtx,
1090
1202
  getRunTestsWrapper,
1203
+ resetRunState,
1091
1204
  newTests,
1092
1205
  efdTests,
1093
1206
  newTestsWithDynamicNames,
@@ -0,0 +1,99 @@
1
+ 'use strict'
2
+
3
+ const { channel } = require('dc-polyfill')
4
+
5
+ const reporterErrorCh = channel('ci:playwright:reporter:error')
6
+ const PLAYWRIGHT_REPORTER_ERROR_MESSAGE = 'Error in reporter'
7
+ const PLAYWRIGHT_REPORTER_ERROR_CALLER_RE =
8
+ /^\s*at wrapAsync .*?[\\/]playwright[\\/]lib[\\/]runner[\\/]index\.js:\d+:\d+\)?$/
9
+
10
+ /**
11
+ * Identifies the private Playwright 1.60-1.61 reporter error fallback without
12
+ * interpreting identical user console output as a framework error.
13
+ *
14
+ * @param {unknown} message - First console.error argument
15
+ * @returns {boolean}
16
+ */
17
+ function isPlaywrightReporterError (message) {
18
+ if (message !== PLAYWRIGHT_REPORTER_ERROR_MESSAGE) return false
19
+
20
+ const originalPrepareStackTrace = Error.prepareStackTrace
21
+ try {
22
+ Error.prepareStackTrace = (_, callSites) => callSites
23
+ const stack = new Error('Playwright reporter error provenance').stack
24
+ return PLAYWRIGHT_REPORTER_ERROR_CALLER_RE.test(`at ${stack?.[2]?.toString() || ''}`)
25
+ } finally {
26
+ Error.prepareStackTrace = originalPrepareStackTrace
27
+ }
28
+ }
29
+
30
+ class DatadogPlaywrightReporter {
31
+ /**
32
+ * Restores console error after Playwright completes the reporter lifecycle.
33
+ *
34
+ * @returns {void}
35
+ */
36
+ static restoreConsoleError () {
37
+ // eslint-disable-next-line no-console
38
+ if (console.error === DatadogPlaywrightReporter.consoleError) {
39
+ try {
40
+ // eslint-disable-next-line no-console
41
+ console.error = DatadogPlaywrightReporter.originalConsoleError
42
+ } catch {}
43
+ }
44
+ DatadogPlaywrightReporter.consoleError = undefined
45
+ DatadogPlaywrightReporter.originalConsoleError = undefined
46
+ }
47
+
48
+ /**
49
+ * Marks the beginning of reporter finalization so later reporter errors can be identified.
50
+ *
51
+ * @returns {void}
52
+ */
53
+ onEnd () {
54
+ this.isFinalizing = true
55
+ // Playwright 1.60 and 1.61 only expose reporter errors through this exact console call.
56
+ // eslint-disable-next-line no-console
57
+ const originalConsoleError = console.error
58
+ const reporter = this
59
+ const consoleError = function (message, error) {
60
+ if (isPlaywrightReporterError(message)) {
61
+ reporter.onError(error)
62
+ }
63
+ return originalConsoleError.apply(this, arguments)
64
+ }
65
+ try {
66
+ // eslint-disable-next-line no-console
67
+ console.error = consoleError
68
+ } catch {
69
+ return
70
+ }
71
+ // An accessor may ignore the assignment without throwing.
72
+ // eslint-disable-next-line no-console
73
+ if (console.error !== consoleError) return
74
+
75
+ DatadogPlaywrightReporter.originalConsoleError = originalConsoleError
76
+ DatadogPlaywrightReporter.consoleError = consoleError
77
+ }
78
+
79
+ /**
80
+ * Reports errors emitted by Playwright while later reporters are finalizing.
81
+ *
82
+ * @param {unknown} error
83
+ * @returns {void}
84
+ */
85
+ onError (error) {
86
+ if (this.isFinalizing) reporterErrorCh.publish(error)
87
+ }
88
+
89
+ /**
90
+ * Keeps the internal reporter from affecting Playwright's output reporter selection.
91
+ *
92
+ * @returns {boolean}
93
+ */
94
+ printsToStdio () {
95
+ return false
96
+ }
97
+ }
98
+
99
+ module.exports = DatadogPlaywrightReporter
@@ -45,6 +45,7 @@ const testSkipCh = channel('ci:playwright:test:skip')
45
45
  const testSessionStartCh = channel('ci:playwright:session:start')
46
46
  const testSessionConfigurationCh = channel('ci:playwright:session:configuration')
47
47
  const testSessionFinishCh = channel('ci:playwright:session:finish')
48
+ const reporterErrorCh = channel('ci:playwright:reporter:error')
48
49
 
49
50
  const libraryConfigurationCh = channel('ci:playwright:library-configuration')
50
51
  const knownTestsCh = channel('ci:playwright:known-tests')
@@ -81,6 +82,8 @@ const isFailureScreenshotUploadEnabled =
81
82
  getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
82
83
 
83
84
  let applyRepeatEachIndex = null
85
+ let reporterError
86
+ let hasReporterError = false
84
87
 
85
88
  let startedSuites = []
86
89
 
@@ -134,8 +137,10 @@ const DD_PROPERTIES_REQUEST = 'ddPropertiesRequest'
134
137
  const DD_PROPERTIES_RESPONSE = 'ddProperties'
135
138
  const kDdPlaywrightDisabledTestIds = Symbol('ddPlaywrightDisabledTestIds')
136
139
  const kDdPlaywrightFailureScreenshots = Symbol('ddPlaywrightFailureScreenshots')
140
+ const kDdPlaywrightReporterConfigured = Symbol('ddPlaywrightReporterConfigured')
137
141
  const kDdPlaywrightWorkerHostInstrumented = Symbol('ddPlaywrightWorkerHostInstrumented')
138
142
  const kDdPlaywrightWorkerInstrumented = Symbol('ddPlaywrightWorkerInstrumented')
143
+ const instrumentedPlaywrightReporters = new WeakSet()
139
144
  const PLAYWRIGHT_FAILURE_SCREENSHOT_PATH_RE = /(?:^|[\\/])test-failed-\d+\.png$/
140
145
  const automaticFailureScreenshotPaths = new Set()
141
146
 
@@ -1252,6 +1257,17 @@ function dispatcherHookNew (dispatcherExport, runWrapper) {
1252
1257
  function runAllTestsWrapper (runAllTests, playwrightVersion) {
1253
1258
  // Config parameter is only available from >=1.55.0
1254
1259
  return async function (config) {
1260
+ reporterError = undefined
1261
+ hasReporterError = false
1262
+ let restoreReporterConsoleError
1263
+ if (satisfies(playwrightVersion, '>=1.60.0') && config?.config) {
1264
+ const DatadogPlaywrightReporter = require('./playwright-reporter')
1265
+ restoreReporterConsoleError = DatadogPlaywrightReporter.restoreConsoleError
1266
+ if (!config.config[kDdPlaywrightReporterConfigured]) {
1267
+ Object.defineProperty(config.config, kDdPlaywrightReporterConfigured, { value: true })
1268
+ config.config.reporter.unshift([require.resolve('./playwright-reporter')])
1269
+ }
1270
+ }
1255
1271
  rootDir = getRootDir(this, config)
1256
1272
  const projects = getProjectsFromRunner(this, config)
1257
1273
  const isFailureScreenshotEnabled = isFailureScreenshotCaptureEnabled(projects)
@@ -1367,7 +1383,27 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1367
1383
  }
1368
1384
  }
1369
1385
 
1370
- let runAllTestsReturn = await runAllTests.apply(this, arguments)
1386
+ let runAllTestsReturn
1387
+ try {
1388
+ runAllTestsReturn = await runAllTests.apply(this, arguments)
1389
+ } catch (error) {
1390
+ try {
1391
+ await getChannelPromise(testSessionFinishCh, {
1392
+ status: 'fail',
1393
+ error: hasReporterError ? reporterError : error,
1394
+ isEarlyFlakeDetectionEnabled,
1395
+ isEarlyFlakeDetectionFaulty,
1396
+ isTestManagementTestsEnabled,
1397
+ })
1398
+ } catch (finalizationError) {
1399
+ log.error('Playwright test session finalization error: %s', finalizationError)
1400
+ }
1401
+ reporterError = undefined
1402
+ hasReporterError = false
1403
+ throw error
1404
+ } finally {
1405
+ restoreReporterConsoleError?.()
1406
+ }
1371
1407
 
1372
1408
  // Tests that have only skipped tests may reach this point
1373
1409
  // Skipped tests may or may not go through `testBegin` or `testEnd`
@@ -1422,12 +1458,21 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1422
1458
  logTestOptimizationSummary({ attemptToFixExecutions, newTestsWithDynamicNames })
1423
1459
  loggedAttemptToFixTests.clear()
1424
1460
 
1461
+ const finalizationError = reporterError
1462
+ const finalStatus = hasReporterError
1463
+ ? 'fail'
1464
+ : (preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus])
1425
1465
  await getChannelPromise(testSessionFinishCh, {
1426
- status: preventedToFail ? 'pass' : STATUS_TO_TEST_STATUS[sessionStatus],
1466
+ status: finalStatus,
1467
+ error: finalizationError,
1427
1468
  isEarlyFlakeDetectionEnabled,
1428
1469
  isEarlyFlakeDetectionFaulty,
1429
1470
  isTestManagementTestsEnabled,
1430
1471
  })
1472
+ if (hasReporterError) {
1473
+ if (typeof runAllTestsReturn === 'string') runAllTestsReturn = 'failed'
1474
+ else runAllTestsReturn.status = 'failed'
1475
+ }
1431
1476
 
1432
1477
  startedSuites = []
1433
1478
  remainingTestsByFile = {}
@@ -1444,6 +1489,8 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1444
1489
  ddPropertiesByTestId.clear()
1445
1490
  ddPropertiesRequestsByTestId.clear()
1446
1491
  disabledTestIds.clear()
1492
+ reporterError = undefined
1493
+ hasReporterError = false
1447
1494
 
1448
1495
  // TODO: we can trick playwright into thinking the session passed by returning
1449
1496
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
@@ -1451,6 +1498,68 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
1451
1498
  }
1452
1499
  }
1453
1500
 
1501
+ /**
1502
+ * Records errors thrown or rejected by Playwright reporters before Playwright converts them into run status.
1503
+ *
1504
+ * @param {object} reportersPackage
1505
+ * @returns {object}
1506
+ */
1507
+ function reportersHook (reportersPackage) {
1508
+ return shimmer.wrap(reportersPackage, 'createReporters', createReporters => async function () {
1509
+ const reporters = await createReporters.apply(this, arguments)
1510
+ for (const reporter of reporters) {
1511
+ if (instrumentedPlaywrightReporters.has(reporter)) continue
1512
+
1513
+ const onEnd = reporter.onEnd
1514
+ const onExit = reporter.onExit
1515
+ try {
1516
+ if (typeof onEnd === 'function') {
1517
+ reporter.onEnd = async function () {
1518
+ try {
1519
+ return await onEnd.apply(this, arguments)
1520
+ } catch (error) {
1521
+ recordReporterError(error)
1522
+ throw error
1523
+ }
1524
+ }
1525
+ }
1526
+ if (typeof onExit === 'function') {
1527
+ reporter.onExit = async function () {
1528
+ try {
1529
+ return await onExit.apply(this, arguments)
1530
+ } catch (error) {
1531
+ recordReporterError(error)
1532
+ throw error
1533
+ }
1534
+ }
1535
+ }
1536
+ } catch {
1537
+ continue
1538
+ }
1539
+ if (reporter.onEnd !== onEnd || reporter.onExit !== onExit) instrumentedPlaywrightReporters.add(reporter)
1540
+ }
1541
+ return reporters
1542
+ }, { replaceGetter: true })
1543
+ }
1544
+
1545
+ /**
1546
+ * Records errors from the reporter multiplexer used by Playwright before 1.38.
1547
+ *
1548
+ * @param {object} reportersPackage
1549
+ * @returns {object}
1550
+ */
1551
+ function reporterMultiplexerHook (reportersPackage) {
1552
+ shimmer.wrap(reportersPackage.Multiplexer.prototype, 'onEnd', onEnd => async function () {
1553
+ try {
1554
+ return await onEnd.apply(this, arguments)
1555
+ } catch (error) {
1556
+ recordReporterError(error)
1557
+ throw error
1558
+ }
1559
+ })
1560
+ return reportersPackage
1561
+ }
1562
+
1454
1563
  function runnerHook (runnerExport, playwrightVersion) {
1455
1564
  shimmer.wrap(
1456
1565
  runnerExport.Runner.prototype,
@@ -1550,6 +1659,32 @@ pageGotoCh.subscribe({
1550
1659
  },
1551
1660
  })
1552
1661
 
1662
+ reporterErrorCh.subscribe((error) => {
1663
+ recordReporterError(error)
1664
+ })
1665
+
1666
+ /**
1667
+ * Records a reporter failure even when the reporter throws a falsy value.
1668
+ *
1669
+ * @param {unknown} error
1670
+ * @returns {void}
1671
+ */
1672
+ function recordReporterError (error) {
1673
+ if (hasReporterError) return
1674
+
1675
+ let normalizedError
1676
+ try {
1677
+ const errorObject = error && typeof error === 'object' ? error : undefined
1678
+ normalizedError = new Error(errorObject?.message || errorObject?.value || String(error))
1679
+ normalizedError.name = errorObject?.name || normalizedError.name
1680
+ normalizedError.stack = errorObject?.stack || normalizedError.stack
1681
+ } catch {
1682
+ normalizedError = new Error('Playwright reporter failed')
1683
+ }
1684
+ reporterError = normalizedError
1685
+ hasReporterError = true
1686
+ }
1687
+
1553
1688
  /**
1554
1689
  * Records a path created by Playwright's automatic screenshot recorder.
1555
1690
  *
@@ -1597,6 +1732,12 @@ if (DD_MAJOR < 6) { // <1.38.0 is only supported up to version 5
1597
1732
  file: 'lib/runner/runner.js',
1598
1733
  versions: ['>=1.31.0 <1.38.0'],
1599
1734
  }, runnerHook)
1735
+
1736
+ addHook({
1737
+ name: '@playwright/test',
1738
+ file: 'lib/reporters/multiplexer.js',
1739
+ versions: ['>=1.18.0 <1.38.0'],
1740
+ }, reporterMultiplexerHook)
1600
1741
  }
1601
1742
 
1602
1743
  addHook({
@@ -1617,6 +1758,12 @@ addHook({
1617
1758
  versions: ['>=1.38.0 <1.60.0'],
1618
1759
  }, runnerHook)
1619
1760
 
1761
+ addHook({
1762
+ name: 'playwright',
1763
+ file: 'lib/runner/reporters.js',
1764
+ versions: ['>=1.38.0 <1.60.0'],
1765
+ }, reportersHook)
1766
+
1620
1767
  addHook({
1621
1768
  name: 'playwright',
1622
1769
  file: 'lib/runner/testRunner.js',
@@ -596,6 +596,7 @@ const visitedParams = new WeakSet()
596
596
  function wrapHandleRequest (original) {
597
597
  return function wrappedHandleRequest (...args) {
598
598
  const req = args[0]
599
+ // eslint-disable-next-line no-restricted-syntax -- arbitrary param names; publishing {} sets a WAF address
599
600
  if (routerParamStartCh.hasSubscribers && !visitedParams.has(req.params) && Object.keys(req.params).length) {
600
601
  visitedParams.add(req.params)
601
602
 
@@ -635,6 +636,7 @@ function wrapParam (original) {
635
636
  args[1] = shimmer.wrapFunction(args[1], (originalFn) => {
636
637
  return function wrappedFn (...fnArgs) {
637
638
  const req = fnArgs[0]
639
+ // eslint-disable-next-line no-restricted-syntax -- arbitrary param names; publishing {} sets a WAF address
638
640
  if (routerParamStartCh.hasSubscribers && Object.keys(req.params).length && !visitedParams.has(req.params)) {
639
641
  visitedParams.add(req.params)
640
642
 
@@ -67,6 +67,7 @@ const coverageWrappedProviders = new WeakSet()
67
67
  const finishWrappedContexts = new WeakSet()
68
68
  const runFilesWrappedPrototypes = new WeakSet()
69
69
  const activeRunFilesContexts = new WeakSet()
70
+ const runErrorsByContext = new WeakMap()
70
71
  let isFlakyTestRetriesEnabled = false
71
72
  let flakyTestRetriesCount = 0
72
73
  let isEarlyFlakeDetectionEnabled = false
@@ -1195,15 +1196,18 @@ function getFinishWrapper (exitOrClose) {
1195
1196
  }
1196
1197
 
1197
1198
  const failedSuites = this.state.getFailedFilepaths()
1198
- let error
1199
- if (failedSuites.length) {
1199
+ const runError = runErrorsByContext.get(this)
1200
+ runErrorsByContext.delete(this)
1201
+ let error = runError
1202
+ if (!error && failedSuites.length) {
1200
1203
  error = new Error(`Test suites failed: ${failedSuites.length}.`)
1201
1204
  }
1202
1205
 
1203
1206
  const flushPromise = getChannelPromise(testSessionFinishCh, {
1204
- status: areAllSuitesSkipped ? 'skip' : getSessionStatus(this.state),
1207
+ status: runError ? 'fail' : (areAllSuitesSkipped ? 'skip' : getSessionStatus(this.state)),
1205
1208
  testCodeCoverageLinesTotal,
1206
1209
  error,
1210
+ isTestSessionFinalizationError: Boolean(runError),
1207
1211
  isEarlyFlakeDetectionEnabled,
1208
1212
  isEarlyFlakeDetectionFaulty,
1209
1213
  isTestManagementTestsEnabled,
@@ -1376,6 +1380,9 @@ function wrapVitestRunFiles (Vitest, frameworkVersion) {
1376
1380
  activeRunFilesContexts.add(this)
1377
1381
  try {
1378
1382
  return await runFiles.apply(this, arguments)
1383
+ } catch (error) {
1384
+ runErrorsByContext.set(this, error)
1385
+ throw error
1379
1386
  } finally {
1380
1387
  activeRunFilesContexts.delete(this)
1381
1388
  }
@@ -1677,6 +1684,7 @@ async function reportTypecheckFile (file, sessionConfiguration, frameworkVersion
1677
1684
  }
1678
1685
 
1679
1686
  await getChannelPromise(testSuiteFinishCh, {
1687
+ deferFlush: true,
1680
1688
  frameworkVersion,
1681
1689
  status: getTypecheckTaskStatus(file),
1682
1690
  ...testSuiteCtx.currentStore,
@@ -343,6 +343,7 @@ function getHostname (store, region) {
343
343
  if (!region) return
344
344
  const { awsParams, awsService } = store
345
345
  switch (awsService) {
346
+ case 'CloudWatchEvents':
346
347
  case 'EventBridge':
347
348
  return `events.${region}.amazonaws.com`
348
349
  case 'SQS':