dd-trace 6.3.0 → 6.5.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 (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -122,28 +122,43 @@ function isMarkedAsUnskippable (test) {
122
122
  return false
123
123
  }
124
124
 
125
- function getJestSuitesToRun (skippableSuites, originalTests, rootDir) {
125
+ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRootDir) {
126
126
  const unskippableSuites = {}
127
127
  const forcedToRunSuites = {}
128
128
 
129
129
  const skippedSuites = []
130
130
  const suitesToRun = []
131
+ const normalizedSkippableSuites = new Set(skippableSuites.map(suite => suite.replaceAll('\\', '/')))
131
132
 
132
133
  for (const test of originalTests) {
133
134
  const relativePath = getTestSuitePath(test.path, rootDir)
134
- const shouldBeSkipped = skippableSuites.includes(relativePath)
135
+ const testRootDir = test?.context?.config?.rootDir || fallbackRootDir
136
+ let fallbackRelativePath
137
+ let skippedSuite = normalizedSkippableSuites.has(relativePath) ? relativePath : undefined
138
+ if (testRootDir && testRootDir !== rootDir) {
139
+ fallbackRelativePath = getTestSuitePath(test.path, testRootDir)
140
+ if (skippedSuite === undefined && normalizedSkippableSuites.has(fallbackRelativePath)) {
141
+ skippedSuite = fallbackRelativePath
142
+ }
143
+ }
135
144
  if (isMarkedAsUnskippable(test)) {
136
145
  suitesToRun.push(test)
137
146
  unskippableSuites[relativePath] = true
138
- if (shouldBeSkipped) {
147
+ if (fallbackRelativePath !== undefined) {
148
+ unskippableSuites[fallbackRelativePath] = true
149
+ }
150
+ if (skippedSuite !== undefined) {
139
151
  forcedToRunSuites[relativePath] = true
152
+ if (fallbackRelativePath !== undefined) {
153
+ forcedToRunSuites[fallbackRelativePath] = true
154
+ }
140
155
  }
141
156
  continue
142
157
  }
143
- if (shouldBeSkipped) {
144
- skippedSuites.push(relativePath)
145
- } else {
158
+ if (skippedSuite === undefined) {
146
159
  suitesToRun.push(test)
160
+ } else {
161
+ skippedSuites.push(skippedSuite)
147
162
  }
148
163
  }
149
164
 
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const { getSegment } = require('../../../dd-trace/src/util')
3
4
  const LangChainHandler = require('./default')
4
5
 
5
6
  class LangChainEmbeddingHandler extends LangChainHandler {
6
7
  extractProvider (instance) {
7
- return instance.constructor.name.split('Embeddings')[0].toLowerCase()
8
+ return getSegment(instance.constructor.name, 'Embeddings', 0).toLowerCase()
8
9
  }
9
10
 
10
11
  extractModel (instance) {
@@ -1,10 +1,11 @@
1
1
  'use strict'
2
2
 
3
+ const { getSegment } = require('../../../dd-trace/src/util')
3
4
  const LangChainHandler = require('./default')
4
5
 
5
6
  class LangChainLanguageModelHandler extends LangChainHandler {
6
7
  extractProvider (instance) {
7
- return typeof instance._llmType === 'function' && instance._llmType().split('-')[0]
8
+ return typeof instance._llmType === 'function' && getSegment(instance._llmType(), '-', 0)
8
9
  }
9
10
 
10
11
  extractModel (instance) {
@@ -15,10 +15,10 @@ function getTokensFromLlmOutput (result) {
15
15
  for (const tokenNames of [['input', 'prompt'], ['output', 'completion'], ['total']]) {
16
16
  let token = 0
17
17
  for (const tokenName of tokenNames) {
18
- const underScore = `${tokenName}_tokens`
18
+ const underscore = `${tokenName}_tokens`
19
19
  const camelCase = `${tokenName}Tokens`
20
20
 
21
- token = tokenUsage[underScore] || tokenUsage[camelCase] || token
21
+ token = tokenUsage[underscore] || tokenUsage[camelCase] || token
22
22
  }
23
23
 
24
24
  tokens[tokenNames[0]] = token
@@ -1,16 +1,26 @@
1
1
  'use strict'
2
2
 
3
+ const { basename } = require('node:path')
4
+
3
5
  const { storage } = require('../../datadog-core')
4
6
  const id = require('../../dd-trace/src/id')
5
7
  const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
8
+ const getConfig = require('../../dd-trace/src/config')
9
+ const {
10
+ SCREENSHOT_UPLOAD_RESULT_ERROR,
11
+ SCREENSHOT_UPLOAD_RESULT_UPLOADED,
12
+ getScreenshotCapturedAtMs,
13
+ getScreenshotUploadResult,
14
+ getScreenshotUploadTag,
15
+ } = require('../../dd-trace/src/ci-visibility/test-screenshot')
6
16
 
7
17
  const {
8
18
  finishAllTraceSpans,
9
19
  getTestSuiteCommonTags,
10
20
  getTestSuitePath,
11
21
  isModifiedTest,
22
+ setRumTestCorrelation,
12
23
  TEST_BROWSER_NAME,
13
- TEST_BROWSER_VERSION,
14
24
  TEST_CODE_OWNERS,
15
25
  TEST_EARLY_FLAKE_ABORT_REASON,
16
26
  TEST_EARLY_FLAKE_ENABLED,
@@ -51,6 +61,21 @@ const {
51
61
  const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
52
62
  const log = require('../../dd-trace/src/log')
53
63
 
64
+ const PLAYWRIGHT_FAILURE_SCREENSHOT_RE = /^test-failed-\d+\.png$/
65
+
66
+ /**
67
+ * Returns whether an attachment is an automatic Playwright failure screenshot.
68
+ *
69
+ * @param {object} attachment - Playwright test attachment
70
+ * @returns {boolean}
71
+ */
72
+ function isPlaywrightFailureScreenshot (attachment) {
73
+ return attachment?.name === 'screenshot' &&
74
+ attachment.contentType === 'image/png' &&
75
+ typeof attachment.path === 'string' &&
76
+ PLAYWRIGHT_FAILURE_SCREENSHOT_RE.test(basename(attachment.path))
77
+ }
78
+
54
79
  class PlaywrightPlugin extends CiPlugin {
55
80
  static id = 'playwright'
56
81
 
@@ -60,6 +85,8 @@ class PlaywrightPlugin extends CiPlugin {
60
85
  this._testSuiteSpansByTestSuiteAbsolutePath = new Map()
61
86
  this.numFailedTests = 0
62
87
  this.numFailedSuites = 0
88
+ this.pendingTestFinishes = 0
89
+ this.finishSession = undefined
63
90
 
64
91
  this.addSub('ci:playwright:test:is-modified', ({
65
92
  filePath,
@@ -71,6 +98,30 @@ class PlaywrightPlugin extends CiPlugin {
71
98
  onDone(isModified)
72
99
  })
73
100
 
101
+ this.addSub('ci:playwright:session:start', ({ isFailureScreenshotEnabled }) => {
102
+ if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED) return
103
+
104
+ if (!isFailureScreenshotEnabled) {
105
+ log.warn(
106
+ '%s %s',
107
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot capture is disabled.',
108
+ 'Set Playwright use.screenshot to "only-on-failure", "on-first-failure", or "on".'
109
+ )
110
+ }
111
+ })
112
+
113
+ this.addSub('ci:playwright:session:configuration', ({ isFailureScreenshotEnabled }) => {
114
+ if (!getConfig().testOptimization.DD_TEST_FAILURE_SCREENSHOTS_ENABLED || !isFailureScreenshotEnabled) return
115
+
116
+ if (!this.tracer._exporter?.canUploadTestScreenshots?.()) {
117
+ log.warn(
118
+ '%s %s',
119
+ 'DD_TEST_FAILURE_SCREENSHOTS_ENABLED is true, but Playwright screenshot upload is not supported',
120
+ 'by the active Test Optimization transport.'
121
+ )
122
+ }
123
+ })
124
+
74
125
  this.addSub('ci:playwright:session:finish', ({
75
126
  status,
76
127
  isEarlyFlakeDetectionEnabled,
@@ -78,42 +129,51 @@ class PlaywrightPlugin extends CiPlugin {
78
129
  isTestManagementTestsEnabled,
79
130
  onDone,
80
131
  }) => {
81
- this.testModuleSpan.setTag(TEST_STATUS, status)
82
- this.testSessionSpan.setTag(TEST_STATUS, status)
132
+ const finishSession = () => {
133
+ this.testModuleSpan.setTag(TEST_STATUS, status)
134
+ this.testSessionSpan.setTag(TEST_STATUS, status)
83
135
 
84
- if (isEarlyFlakeDetectionEnabled) {
85
- this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
86
- }
87
- if (isEarlyFlakeDetectionFaulty) {
88
- this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
89
- }
90
- if (status === 'fail' && this.numFailedSuites > 0) {
91
- let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
92
- if (this.numFailedTests > 0) {
93
- errorMessage += ` Tests failed: ${this.numFailedTests}`
136
+ if (isEarlyFlakeDetectionEnabled) {
137
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ENABLED, 'true')
138
+ }
139
+ if (isEarlyFlakeDetectionFaulty) {
140
+ this.testSessionSpan.setTag(TEST_EARLY_FLAKE_ABORT_REASON, 'faulty')
141
+ }
142
+ if (status === 'fail' && this.numFailedSuites > 0) {
143
+ let errorMessage = `Test suites failed: ${this.numFailedSuites}.`
144
+ if (this.numFailedTests > 0) {
145
+ errorMessage += ` Tests failed: ${this.numFailedTests}`
146
+ }
147
+ const error = new Error(errorMessage)
148
+ this.testModuleSpan.setTag('error', error)
149
+ this.testSessionSpan.setTag('error', error)
94
150
  }
95
- const error = new Error(errorMessage)
96
- this.testModuleSpan.setTag('error', error)
97
- this.testSessionSpan.setTag('error', error)
98
- }
99
151
 
100
- if (isTestManagementTestsEnabled) {
101
- this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
152
+ if (isTestManagementTestsEnabled) {
153
+ this.testSessionSpan.setTag(TEST_MANAGEMENT_ENABLED, 'true')
154
+ }
155
+
156
+ this.testModuleSpan.finish()
157
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
158
+ this.testSessionSpan.finish()
159
+ this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
160
+ finishAllTraceSpans(this.testSessionSpan)
161
+ this.telemetry.count(TELEMETRY_TEST_SESSION, {
162
+ provider: this.ciProviderName,
163
+ autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
164
+ })
165
+ appClosingTelemetry()
166
+ this.tracer._exporter.flush(onDone)
167
+ this.numFailedTests = 0
168
+ this.numFailedSuites = 0
169
+ this.finishSession = undefined
102
170
  }
103
171
 
104
- this.testModuleSpan.finish()
105
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
106
- this.testSessionSpan.finish()
107
- this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
108
- finishAllTraceSpans(this.testSessionSpan)
109
- this.telemetry.count(TELEMETRY_TEST_SESSION, {
110
- provider: this.ciProviderName,
111
- autoInjected: !!this._tracerConfig.testOptimization.DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER,
112
- })
113
- appClosingTelemetry()
114
- this.tracer._exporter.flush(onDone)
115
- this.numFailedTests = 0
116
- this.numFailedSuites = 0
172
+ if (this.pendingTestFinishes > 0) {
173
+ this.finishSession = finishSession
174
+ } else {
175
+ finishSession()
176
+ }
117
177
  })
118
178
 
119
179
  this.addBind('ci:playwright:test-suite:start', (ctx) => {
@@ -178,42 +238,17 @@ class PlaywrightPlugin extends CiPlugin {
178
238
  this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
179
239
  })
180
240
 
181
- this.addSub('ci:playwright:test:page-goto', ({
182
- isRumActive,
183
- page,
184
- }) => {
185
- const store = storage('legacy').getStore()
186
- const span = store && store.span
187
- if (!span) {
241
+ this.addSub('ci:playwright:test:page-goto', (ctx) => {
242
+ const activeSpan = storage('legacy').getStore()?.span
243
+ if (!setRumTestCorrelation(ctx, activeSpan)) {
188
244
  log.error('ci:playwright:test:page-goto: test span not found')
189
- return
190
- }
191
-
192
- if (isRumActive) {
193
- span.setTag(TEST_IS_RUM_ACTIVE, 'true')
194
-
195
- if (page) {
196
- const browserVersion = page.context().browser().version()
197
-
198
- if (browserVersion) {
199
- span.setTag(TEST_BROWSER_VERSION, browserVersion)
200
- }
201
-
202
- const url = page.url()
203
- const domain = new URL(url).hostname
204
- page.context().addCookies([{
205
- name: 'datadog-ci-visibility-test-execution-id',
206
- value: span.context().toTraceId(),
207
- domain,
208
- path: '/',
209
- }])
210
- }
211
245
  }
212
246
  })
213
247
 
214
- this.addSub('ci:playwright:worker:report', (serializedTraces) => {
248
+ this.addSub('ci:playwright:worker:report', ({ serializedTraces, screenshots }) => {
215
249
  const traces = JSON.parse(serializedTraces)
216
250
  const formattedTraces = []
251
+ let formattedTestSpan
217
252
 
218
253
  for (const trace of traces) {
219
254
  const formattedTrace = []
@@ -228,6 +263,7 @@ class PlaywrightPlugin extends CiPlugin {
228
263
  formattedSpan.meta[TEST_HAS_DYNAMIC_NAME] = 'true'
229
264
  }
230
265
  if (span.name === 'playwright.test') {
266
+ formattedTestSpan = formattedSpan
231
267
  // TODO: remove this comment
232
268
  // TODO: Let's pass rootDir, repositoryRoot, command, session id and module id as env vars
233
269
  // so we don't need this re-serialization logic. This can be passed just once, since they're unique
@@ -262,9 +298,36 @@ class PlaywrightPlugin extends CiPlugin {
262
298
  formattedTraces.push(formattedTrace)
263
299
  }
264
300
 
265
- for (const trace of formattedTraces) {
266
- this.tracer._exporter.export(trace)
301
+ const exportTraces = () => {
302
+ for (const trace of formattedTraces) {
303
+ this.tracer._exporter.export(trace)
304
+ }
267
305
  }
306
+
307
+ if (!formattedTestSpan || !screenshots) {
308
+ exportTraces()
309
+ return
310
+ }
311
+
312
+ this.pendingTestFinishes++
313
+ const uploadStarted = this.uploadTestScreenshots({
314
+ screenshots,
315
+ traceId: formattedTestSpan.trace_id.toString(10),
316
+ }, (screenshotUploadResult) => {
317
+ const screenshotUploadTag = getScreenshotUploadTag(screenshotUploadResult)
318
+ if (screenshotUploadTag) {
319
+ formattedTestSpan.meta[screenshotUploadTag] = 'true'
320
+ }
321
+ exportTraces()
322
+ this.pendingTestFinishes--
323
+ if (this.pendingTestFinishes === 0 && this.finishSession) {
324
+ this.finishSession()
325
+ }
326
+ })
327
+ if (uploadStarted) return
328
+
329
+ this.pendingTestFinishes--
330
+ exportTraces()
268
331
  })
269
332
 
270
333
  this.addBind('ci:playwright:test:start', (ctx) => {
@@ -480,6 +543,56 @@ class PlaywrightPlugin extends CiPlugin {
480
543
  })
481
544
  }
482
545
 
546
+ /**
547
+ * Uploads automatic failure screenshots for a Playwright test attempt.
548
+ *
549
+ * @param {object} options - Upload options
550
+ * @param {Array<object>} options.screenshots - Playwright test attachments
551
+ * @param {string} options.traceId - Test trace id used as the screenshot key
552
+ * @param {(result: string|undefined) => void} onDone - Completion callback
553
+ * @returns {boolean} Whether at least one upload was started
554
+ */
555
+ uploadTestScreenshots ({ screenshots, traceId }, onDone) {
556
+ const exporter = this.tracer?._exporter
557
+ if (!Array.isArray(screenshots) || !screenshots.length ||
558
+ !exporter?.canUploadTestScreenshots?.() ||
559
+ !exporter.uploadTestScreenshot) {
560
+ return false
561
+ }
562
+
563
+ const screenshotPaths = new Set()
564
+ for (const screenshot of screenshots) {
565
+ if (isPlaywrightFailureScreenshot(screenshot)) {
566
+ screenshotPaths.add(screenshot.path)
567
+ }
568
+ }
569
+ if (!screenshotPaths.size) return false
570
+
571
+ const uploadResults = new Array(screenshotPaths.size)
572
+ let pendingUploads = screenshotPaths.size
573
+ let index = 0
574
+ for (const filePath of screenshotPaths) {
575
+ const resultIndex = index++
576
+ exporter.uploadTestScreenshot({
577
+ filePath,
578
+ traceId,
579
+ idempotencyKey: `${traceId}:${basename(filePath)}`,
580
+ capturedAtMs: getScreenshotCapturedAtMs(filePath, filePath),
581
+ }, (error, uploaded = true) => {
582
+ if (uploaded) {
583
+ uploadResults[resultIndex] = error
584
+ ? SCREENSHOT_UPLOAD_RESULT_ERROR
585
+ : SCREENSHOT_UPLOAD_RESULT_UPLOADED
586
+ }
587
+ pendingUploads--
588
+ if (pendingUploads === 0) {
589
+ onDone(getScreenshotUploadResult(uploadResults))
590
+ }
591
+ })
592
+ }
593
+ return true
594
+ }
595
+
483
596
  // TODO: this runs both in worker and main process (main process: skipped tests that do not go through _runTest)
484
597
  startTestSpan (
485
598
  testName,
@@ -24,7 +24,7 @@ class RouterPlugin extends WebPlugin {
24
24
  let childOf
25
25
  if (context !== undefined) {
26
26
  const middleware = context.middleware
27
- childOf = middleware.length === 0 ? context.span : middleware[middleware.length - 1]
27
+ childOf = middleware.length === 0 ? context.span : middleware.at(-1)
28
28
  } else if (store) {
29
29
  childOf = store.span
30
30
  }
@@ -75,12 +75,21 @@ class RouterPlugin extends WebPlugin {
75
75
  const context = this.#contexts.get(req)
76
76
  if (!context) return
77
77
  const middleware = context.middleware
78
- const span = middleware.length === 0 ? context.span : middleware[middleware.length - 1]
78
+ const span = middleware.length === 0 ? context.span : middleware.at(-1)
79
79
  if (!span) return
80
80
 
81
81
  span.setTag('error', error)
82
82
  })
83
83
 
84
+ this.addSub(`apm:${this.constructor.id}:middleware:repeat`, ({ req, name, error }) => {
85
+ // The middleware span already finished on the first `next`, so record the
86
+ // repeat on the still-live request span instead of a finished one.
87
+ web.root(req)?.addEvent('middleware.next_called_again', {
88
+ 'middleware.name': name || '<anonymous>',
89
+ with_error: Boolean(error && error !== 'route' && error !== 'router'),
90
+ })
91
+ })
92
+
84
93
  this.addSub('apm:http:server:request:finish', ({ req }) => {
85
94
  const context = this.#contexts.get(req)
86
95
 
@@ -4,27 +4,12 @@ const CiPlugin = require('../../dd-trace/src/plugins/ci_plugin')
4
4
  const { storage } = require('../../datadog-core')
5
5
 
6
6
  const {
7
- TEST_IS_RUM_ACTIVE,
7
+ setRumTestCorrelation,
8
8
  TEST_BROWSER_DRIVER,
9
9
  TEST_BROWSER_DRIVER_VERSION,
10
10
  TEST_BROWSER_NAME,
11
- TEST_BROWSER_VERSION,
12
11
  TEST_TYPE,
13
12
  } = require('../../dd-trace/src/plugins/util/test')
14
- const { SPAN_TYPE } = require('../../../ext/tags')
15
-
16
- function isTestSpan (span) {
17
- return span.context().getTag(SPAN_TYPE) === 'test'
18
- }
19
-
20
- function getTestSpanFromTrace (trace) {
21
- for (const span of trace.started) {
22
- if (isTestSpan(span)) {
23
- return span
24
- }
25
- }
26
- return null
27
- }
28
13
 
29
14
  class SeleniumPlugin extends CiPlugin {
30
15
  static id = 'selenium'
@@ -32,32 +17,16 @@ class SeleniumPlugin extends CiPlugin {
32
17
  constructor (...args) {
33
18
  super(...args)
34
19
 
35
- this.addSub('ci:selenium:driver:get', ({
36
- setTraceId,
37
- seleniumVersion,
38
- browserName,
39
- browserVersion,
40
- isRumActive,
41
- }) => {
42
- const store = storage('legacy').getStore()
43
- const span = store?.span
44
- if (!span) {
45
- return
46
- }
47
- const testSpan = isTestSpan(span) ? span : getTestSpanFromTrace(span.context()._trace)
20
+ this.addSub('ci:selenium:driver:get', (ctx) => {
21
+ const { seleniumVersion, browserName } = ctx
22
+ const activeSpan = storage('legacy').getStore()?.span
23
+ const testSpan = setRumTestCorrelation(ctx, activeSpan)
48
24
  if (!testSpan) {
49
25
  return
50
26
  }
51
- if (setTraceId) {
52
- setTraceId(testSpan.context().toTraceId())
53
- }
54
- if (isRumActive) {
55
- testSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
56
- }
57
27
  testSpan.setTag(TEST_BROWSER_DRIVER, 'selenium')
58
28
  testSpan.setTag(TEST_BROWSER_DRIVER_VERSION, seleniumVersion)
59
29
  testSpan.setTag(TEST_BROWSER_NAME, browserName)
60
- testSpan.setTag(TEST_BROWSER_VERSION, browserVersion)
61
30
  testSpan.setTag(TEST_TYPE, 'browser')
62
31
  })
63
32
  }
@@ -264,8 +264,10 @@ class VitestPlugin extends CiPlugin {
264
264
  span.setTag(TEST_EARLY_FLAKE_ABORT_REASON, earlyFlakeAbortReason)
265
265
  }
266
266
  const finish = () => {
267
- if (duration) {
268
- span.finish(span._startTime + duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION) // milliseconds
267
+ if (Number.isFinite(duration) && duration >= 0) {
268
+ span.finish(
269
+ span._startTime + Math.max(duration - MILLISECONDS_TO_SUBTRACT_FROM_FAILED_TEST_DURATION, 0)
270
+ ) // milliseconds
269
271
  } else {
270
272
  span.finish() // `duration` is empty for retries, so we'll use clock time
271
273
  }
@@ -6,6 +6,7 @@ const {
6
6
  SPAN_POINTER_DIRECTION,
7
7
  SPAN_POINTER_DIRECTION_NAME,
8
8
  } = require('../../dd-trace/src/constants')
9
+ const { getSegment } = require('../../dd-trace/src/util')
9
10
  const {
10
11
  incrementWebSocketCounter,
11
12
  buildWebSocketSpanPointerHash,
@@ -29,7 +30,7 @@ class WSClosePlugin extends TracingPlugin {
29
30
 
30
31
  const spanKind = isPeerClose ? 'consumer' : 'producer'
31
32
  const spanTags = socket.spanTags
32
- const path = spanTags['resource.name'].split(' ')[1]
33
+ const path = getSegment(spanTags['resource.name'], ' ', 1)
33
34
  const service = this.serviceName({ pluginConfig: this.config })
34
35
  const span = this.startSpan(this.operationName(), {
35
36
  service,
@@ -6,6 +6,7 @@ const {
6
6
  SPAN_POINTER_DIRECTION,
7
7
  SPAN_POINTER_DIRECTION_NAME,
8
8
  } = require('../../dd-trace/src/constants')
9
+ const { getSegment } = require('../../dd-trace/src/util')
9
10
  const {
10
11
  incrementWebSocketCounter,
11
12
  buildWebSocketSpanPointerHash,
@@ -23,7 +24,7 @@ class WSProducerPlugin extends TracingPlugin {
23
24
  if (!socket.spanContext) return
24
25
 
25
26
  const spanTags = socket.spanTags
26
- const path = spanTags['resource.name'].split(' ')[1]
27
+ const path = getSegment(spanTags['resource.name'], ' ', 1)
27
28
  const opCode = binary ? 'binary' : 'text'
28
29
  const service = this.serviceName({ pluginConfig: this.config })
29
30
  const span = this.startSpan(this.operationName(), {
@@ -6,6 +6,7 @@ const {
6
6
  SPAN_POINTER_DIRECTION,
7
7
  SPAN_POINTER_DIRECTION_NAME,
8
8
  } = require('../../dd-trace/src/constants')
9
+ const { getSegment } = require('../../dd-trace/src/util')
9
10
  const {
10
11
  incrementWebSocketCounter,
11
12
  buildWebSocketSpanPointerHash,
@@ -28,7 +29,7 @@ class WSReceiverPlugin extends TracingPlugin {
28
29
  if (!socket.spanContext) return
29
30
 
30
31
  const spanTags = socket.spanTags
31
- const path = spanTags['resource.name'].split(' ')[1]
32
+ const path = getSegment(spanTags['resource.name'], ' ', 1)
32
33
  const opCode = binary ? 'binary' : 'text'
33
34
 
34
35
  const service = this.serviceName({ pluginConfig: this.config })
@@ -3,6 +3,7 @@
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
4
4
  const tags = require('../../../ext/tags.js')
5
5
  const { HTTP_HEADERS } = require('../../../ext/formats')
6
+ const { getSegment } = require('../../dd-trace/src/util')
6
7
  const {
7
8
  createWebSocketSpanContext,
8
9
  hasTraceHeaders,
@@ -94,7 +95,7 @@ function getRequestProtocol (req, fallback = 'ws') {
94
95
 
95
96
  // 2. Check for a trusted header set by a proxy
96
97
  if (req.headers && req.headers['x-forwarded-proto']) {
97
- const proto = req.headers['x-forwarded-proto'].split(',')[0].trim()
98
+ const proto = getSegment(req.headers['x-forwarded-proto'], ',', 0).trim()
98
99
  if (proto === 'https') return 'wss'
99
100
  if (proto === 'http') return 'ws'
100
101
  }
@@ -0,0 +1,3 @@
1
+ 'use strict'
2
+
3
+ module.exports = require('./src/bootstrap')
@@ -1,39 +1,4 @@
1
1
  'use strict'
2
2
 
3
- if (!global._ddtrace) {
4
- const ddTraceSymbol = Symbol.for('dd-trace')
5
-
6
- // Set up beforeExitHandlers before loading the tracer so that modules loaded
7
- // during require('./src') can register handlers.
8
- Object.defineProperty(globalThis, ddTraceSymbol, {
9
- value: {
10
- beforeExitHandlers: new Set(),
11
- },
12
- enumerable: false,
13
- configurable: true,
14
- writable: false,
15
- })
16
-
17
- process.once('beforeExit', function mainBeforeExit () {
18
- if (globalThis[ddTraceSymbol]?.beforeExitHandlers) {
19
- for (const handler of globalThis[ddTraceSymbol].beforeExitHandlers) {
20
- handler()
21
- }
22
- }
23
- })
24
-
25
- const TracerProxy = require('./src')
26
-
27
- Object.defineProperty(global, '_ddtrace', {
28
- value: new TracerProxy(),
29
- enumerable: false,
30
- configurable: true,
31
- writable: true,
32
- })
33
- }
34
-
35
- module.exports = global._ddtrace
36
- // Static aliases so cjs-module-lexer surfaces them as ESM named exports
37
- // (`import { tracer } from 'dd-trace'`).
38
- module.exports.tracer = global._ddtrace
39
- module.exports.default = global._ddtrace
3
+ require('./src/openfeature/register')
4
+ module.exports = require('./src/bootstrap')
@@ -230,7 +230,7 @@ class AIGuard extends NoopAIGuard {
230
230
  // still applies for SDK callers that don't supply an explicit parent.
231
231
  const traceOpts = childOf ? { childOf } : {}
232
232
  return this.#tracer.trace(TAGS.RESOURCE, traceOpts, async (span) => {
233
- const last = messages[messages.length - 1]
233
+ const last = messages.at(-1)
234
234
  const target = this.#isToolCall(last) ? 'tool' : 'prompt'
235
235
  span.setTag(TAGS.TARGET_TAG_KEY, target)
236
236
  if (target === 'tool') {
@@ -190,7 +190,7 @@ class TaintTrackingPlugin extends SourceIastPlugin {
190
190
  }
191
191
 
192
192
  addURLParsingSubscriptions () {
193
- const urlResultTaintedProperties = ['host', 'origin', 'hostname']
193
+ const urlResultTaintedProperties = new Set(['host', 'origin', 'hostname'])
194
194
  this.addSub(
195
195
  { channelName: 'datadog:url:parse:finish' },
196
196
  ({ input, base, parsed, isURL }) => {
@@ -212,7 +212,7 @@ class TaintTrackingPlugin extends SourceIastPlugin {
212
212
  this.addSub(
213
213
  { channelName: 'datadog:url:getter:finish' },
214
214
  (context) => {
215
- if (!urlResultTaintedProperties.includes(context.property)) return
215
+ if (!urlResultTaintedProperties.has(context.property)) return
216
216
 
217
217
  const origRange = this._taintedURLs.get(context.urlObject)
218
218
  if (!origRange) return